Reputation: 11
I am trying to implement map functionality into my app. I have run into issues with MapKit when using either UIKit and SwiftUI. Here's my problem. I have an @Published array called events inside a class called EventViewModel. I pass this view model as an EnvironmentObject as I use it in many views. In one view I take in user input and add an Event to the array. This is working properly, my Events render in a list design. However, I am also using the events array as annotationItems in my Map() as an event contains coordinates for a location. My problem is when I add a new event, a new annotation does not render on the map. However, if I have preloaded data in the @Published events array, the annotations load fine.
Here's the SwiftUI interfaced with UIKit
import SwiftUI
struct MapView: View {
@EnvironmentObject var eventViewModel: EventViewModel
var body: some View {
MapViewController(eventViewModel: eventViewModel)
.ignoresSafeArea()
}
}
struct MapViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = ViewController
let eventViewModel: EventViewModel
func makeUIViewController(context: Context) -> ViewController {
let vc = ViewController()
vc.eventViewModel = eventViewModel
return vc
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
}
}
class ViewController: UIViewController {
var locationManager: CLLocationManager?
var eventViewModel: EventViewModel? {
didSet {
subscribeToEventChanges()
}
}
private var cancellables: Set<AnyCancellable> = []
private func subscribeToEventChanges() {
eventViewModel?.$events
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateMapAnnotations()
}
.store(in: &cancellables)
}
private func updateMapAnnotations() {
// Clear existing annotations on the map
mapView.removeAnnotations(mapView.annotations)
// Add new annotations based on the updated eventViewModel.events
addEventPins()
print("Update Map Annotations") // Get's into this function when an event is added
}
lazy var mapView: MKMapView = {
let map = MKMapView()
map.showsUserLocation = true
map.isRotateEnabled = false
map.translatesAutoresizingMaskIntoConstraints = false
return map
}()
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.requestWhenInUseAuthorization()
locationManager?.requestLocation()
mapView.delegate = self
createMap()
addEventPins()
}
private func createMap() {
view.addSubview(mapView)
mapView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
mapView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
mapView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
mapView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
private func checkLocationAuthorization() {
guard let locationManager = locationManager,
let location = locationManager.location else { return }
switch locationManager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
let region = MKCoordinateRegion(
center: location.coordinate,
latitudinalMeters: 750,
longitudinalMeters: 750
)
mapView.setRegion(region, animated: true)
case .notDetermined, .restricted:
print("Location cannot be determined or is restricted")
case .denied:
print("Location services have been denied.")
@unknown default:
print("Unknown error occurred. Unable to get location.")
}
}
private func addEventPins() {
guard let eventViewModel = eventViewModel else { return }
for event in eventViewModel.events {
let annotation = MKPointAnnotation()
annotation.coordinate = event.coordinate
annotation.title = event.name
annotation.subtitle = event.host
let _ = LocationAnnotationView(annotation: annotation, reuseIdentifier: "eventAnnotation")
mapView.addAnnotation(annotation)
print(mapView.annotations) // Properly adds the annotation when an event is added
}
}
}
final class LocationAnnotationView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
centerOffset = CGPoint(x: -125, y: -125)
canShowCallout = true
setupUI(name: annotation?.title ?? "")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI(name: String?) {
backgroundColor = .clear
let hostingController = UIHostingController(rootView: Pin(name: name ?? ""))
hostingController.view.frame = CGRect(x: 0, y: 0, width: 250, height: 250)
hostingController.view.transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
addSubview(hostingController.view)
hostingController.view.backgroundColor = .clear
}
}
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "locationAnnotation")
if annotationView == nil {
annotationView = LocationAnnotationView(annotation: annotation, reuseIdentifier: "locationAnnotation")
} else {
annotationView?.annotation = annotation
}
return annotationView
}
}
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorization()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
}
Lastly here is the createEvent() function I have in another view, it also uses
@EnvironmentObject var eventViewModel: EventViewModel
private func createEvent() {
var coordinates = CLLocationCoordinate2D()
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(self.location) {
placemarks, error in
let placemark = placemarks?.first
let lat = placemark?.location?.coordinate.latitude
let lon = placemark?.location?.coordinate.longitude
// Format is Address, City, State Zipcode
coordinates = CLLocationCoordinate2D(latitude: lon ?? CLLocationDegrees(), longitude: lat ?? CLLocationDegrees())
print(coordinates)
let event = Event(name: self.eventName, host: self.hosts.first ?? "No Host", date: self.date, coordinate: coordinates)
let date = Calendar.current.startOfDay(for: event.date)
eventViewModel.groupedEvents[date, default: []].append(event)
eventViewModel.events.append(event)
print(eventViewModel.events)
}
}
At first I started completely with the SwiftUI version of MapKit, but found the same issue of annotations not updating when an event is appended to the array. Made the switch to UIKit as many people said it gives more flexibility as the current version of MapKit is still fairly limited.
Upvotes: 0
Views: 130
Reputation: 30549
Just change:
let eventViewModel: EventViewModel
to
let events: [Event]
Then implement:
func updateUIViewController(...
This will be called whenever the events
change. Remove any from the map that are no longer in the array and add any new ones.
We don't use view model objects in SwiftUI, the View
struct is the view model already. Also, a Combine pipeline going down the sink
is usually a design flaw, if you implement update
then you don't need any Combine.
Upvotes: 0