0

I have MapAnnotationView class:

final class MapAnnotationView: MKAnnotationView {
    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        
        frame = CGRect(x: 0, y: 0, width: 50, height: 50)
        centerOffset = CGPoint(x: 0, y: -frame.size.height / 2)
    }

    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupUI(with annotation: PlaceAnnotation) {
        backgroundColor = .clear
        
        let vc = UIHostingController(rootView: MapPin(color: Color(hex: annotation.color ?? 0),
                                                         imageSystemName: annotation.icon ?? ""))
        vc.view.backgroundColor = .clear
        addSubview(vc.view)

        vc.view.frame = bounds
    }
}

MapPin used as HostingController:

struct MapPinShape: Shape {
    func path(in rect: CGRect) -> Path {
        let radius = rect.width / 2
        var path = Path()
        path.addArc(center: CGPoint(x: rect.midX, y: rect.minY + radius), radius: radius, startAngle: .degrees(70), endAngle: .degrees(110), clockwise: true)
        path.addLine(to: CGPoint(x: rect.midX, y: radius * 2 + 0.2 * radius))

        return path
    }
}

struct MapPin: View {
    var color: Color
    var imageSystemName: String
    
    var body: some View {
        ZStack {
            MapPinShape()
                .fill(color.darker())
            GeometryReader { geo in
                Circle().fill(color)
                    .frame(width: geo.size.width * 0.90)
                    .frame(width: geo.size.width, height: geo.size.height, alignment: .center)
                Image(systemName: imageSystemName)
                    .resizable()
                    .scaledToFit()
                    .foregroundColor(.white)
                    .frame(width: geo.size.width / 3)
                    .frame(width: geo.size.width, height: geo.size.height, alignment: .center)
            }
        }
    }
}

It is used in MKMap that is in UIViewRepresentable:

struct MapView: UIViewRepresentable {
    var annotations: [PlaceAnnotation]
    var trackingMode: MKUserTrackingMode
    var onLongPress: (_ location: MapLocation) -> Void
    var onAnnotationTap: (_ annotation: PlaceAnnotation) -> Void
    
    let mapView = MKMapView()
    let locationManager = CLLocationManager()

    func makeUIView(context: Context) -> MKMapView {
        locationManager.delegate = context.coordinator
        
        mapView.delegate = context.coordinator
        mapView.showsUserLocation = true
        mapView.register(MapAnnotationView.self, forAnnotationViewWithReuseIdentifier: "AnnotationView")
        mapView.register(MapClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: "ClusterAnnotationView")
        
        let longPressRecognizer = UILongPressGestureRecognizer(target: context.coordinator,
                                                               action: #selector(Coordinator.longPressed(_:)))
        mapView.addGestureRecognizer(longPressRecognizer)
        
        configureLocationServices()
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        let existing = mapView.annotations.compactMap { $0 as? EntMap.PlaceAnnotation }
        let diff = annotations.difference(from: existing) { $0 === $1 }

        for change in diff {
            switch change {
            case .insert(_, let element, _): mapView.addAnnotation(element)
            case .remove(_, let element, _): mapView.removeAnnotation(element)
            }
        }
    }
    
    func makeCoordinator() -> MapViewCoordinator {
        MapViewCoordinator(parent: self)
    }
    
    func centerMapOnUserLocation() {
        guard let coordinate = locationManager.location?.coordinate else {return}
        let coordinateRegion = MKCoordinateRegion(center: coordinate, latitudinalMeters: 1000, longitudinalMeters: 1000)
        mapView.setRegion(coordinateRegion, animated: true)
    }
    
    private func configureLocationServices() {
        if locationManager.authorizationStatus == .notDetermined {
            locationManager.requestAlwaysAuthorization()
        } else {
            return
        }
    }
}

class MapViewCoordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
    let parent: MapView
    
    init(parent: MapView) {
        self.parent = parent
    }
    
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        if let annotation = annotation as? EntMap.PlaceAnnotation {
            if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "AnnotationView", for: annotation) as? MapAnnotationView {
                let mapAnnotation = annotation as EntMap.PlaceAnnotation
                annotationView.clusteringIdentifier = "ClusterAnnotationView"
                annotationView.setupUI(with: mapAnnotation)
                
                return annotationView
            }
        }

        if let annotation = annotation as? MKClusterAnnotation {
            if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "ClusterAnnotationView") as? MapClusterAnnotationView {
                annotationView.annotation = annotation
                let count = annotation.memberAnnotations.count < 100 ? "\(annotation.memberAnnotations.count)" : "99+"
                annotationView.setupUI(count: count)
                
                return annotationView
            }
        }
        
        return nil
    }
    
    func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
        for view in views {
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(annotationTapped))
            view.addGestureRecognizer(tapGesture)
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        parent.centerMapOnUserLocation()
    }
    
    @objc func annotationTapped(_ gestureRecognizer: UITapGestureRecognizer) {
        if let view = gestureRecognizer.view as? MKAnnotationView {
            if let annotation = view.annotation as? MKClusterAnnotation {
                parent.mapView.showAnnotations(annotation.memberAnnotations, animated: true)
            }
            
            if let annotation = view.annotation as? EntMap.PlaceAnnotation {
                parent.onAnnotationTap(annotation)
            }
        }
    }
    
    @objc func longPressed(_ gestureRecognizer: UIGestureRecognizer) {
        if gestureRecognizer.state == .ended {
            if let map = gestureRecognizer.view as? MKMapView {
                let tapPoint = gestureRecognizer.location(in: map)
                let tapCoordinate = map.convert(tapPoint, toCoordinateFrom: map)
                parent.onLongPress(MapLocation(coordinates: tapCoordinate))
            }
        }
    }
}

I have a problem that annotation view is breaking when it must render on the edge of map. The view is being fixed when it is closer to center. I believe the problem is in the MapPin view or in the UIHostingView in setupUI method but I can't figure it out why exactly. broken annotation fixed annotation

I have tried to change the SwiftUI with UIKit with simpler custom view and I have not noticed the bug.

Bartson
  • 93
  • 1
  • 8
  • Might be unrelated but that UIViewRepresentable has a few mistakes, e.g. make needs to init and return the map. Also MapViewCoordinator cannot be init with self because it is immediately out of date. Might be worth doing a refresher on value types like structs. – malhal Mar 26 '23 at 15:25
  • in `func setupUI`, it seems you are not holding a reference to the UIHostingController so it will automatically deallocated and doesn't exist any more. See https://stackoverflow.com/questions/40636997/add-view-controller-to-container-view-overwriting-view ˚addChildViewController` – Gerd Castan Apr 03 '23 at 09:38

0 Answers0