MKMapView not Clustering Annotation on zooming out map in Swift
Asked Answered
M

1

7

before duplicating my questing please read the whole thing. i am using MkMapKit in my app and now I have to show people in cluster when zoom out the map, I have achieved numbering them out so far using from this answer using Apple's default clustering class. now I have no idea how to add and show them all one circle with there count, i know something that it should relate with radius but i don't know how can i do that, sharing my code below, i hope any help would be appreciated. thanks also showing picture of what i have done :

enter image description here

This is my UserAnnotationClass

class UserAnnotation: NSObject, MKAnnotation {

    let title: String?
    let locationName: String
    let discipline: String
    let coordinate: CLLocationCoordinate2D

    let userProfile: UserProfile!
    let index: Int!
    let memberAnnotations: [UserProfile]!
    init(userProfile: UserProfile, at index: Int) {

        self.title = userProfile.fullName
        self.locationName = (userProfile.locationAddress != nil) ? userProfile.locationAddress : ""
        let userProfilePicture: String = (userProfile.profilePicture == nil || userProfile.profilePicture == "") ? "" : userProfile.profilePicture

        self.discipline = userProfilePicture

       // print("\(userProfile.fullName) \(userProfile.location.dist)")

        if (userProfile.isMapVisibility == true) {
            self.coordinate = CLLocationCoordinate2D(latitude: userProfile.location.lat, longitude: userProfile.location.lon)
        } else {
            self.coordinate = CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)
        }
        memberAnnotations = [UserProfile]()
        memberAnnotations.append(userProfile)
        self.userProfile = userProfile
        self.index = index

        super.init()
    }

    var subtitle: String? {
        return locationName
    }

    // pinTintColor for disciplines: Sculpture, Plaque, Mural, Monument, other
    var markerTintColor: UIColor  {
        switch discipline {
        case "Monument":
            return .red
        case "Mural":
            return .cyan
        case "Plaque":
            return .blue
        case "Sculpture":
            return .purple
        default:
            return .clear
        }
    }

    // Annotation right callout accessory opens this mapItem in Maps app
    func mapItem() -> MKMapItem {
        let addressDict = [CNPostalAddressStreetKey: subtitle!]
        let placemark = MKPlacemark(coordinate: coordinate, addressDictionary: addressDict)
        let mapItem = MKMapItem(placemark: placemark)
        mapItem.name = title


        return mapItem
    }
}

And This is the CLusterViewClass i am using to make them clustered .

class ClusterView: MKAnnotationView {


    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let hitView = super.hitTest(point, with: event)
        if (hitView != nil)
        {

            if (hitView?.isKind(of: UIButton.self))! {

                let sender: UIButton = hitView as! UIButton

                sender.sendActions(for: .touchUpInside)

            }
            else {

                self.superview?.bringSubviewToFront(self)
            }
        }
        return hitView
    }
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let rect = self.bounds
        var isInside: Bool = rect.contains(point)
        if(!isInside)
        {
            for view in self.subviews
            {
                isInside = view.frame.contains(point)
                if isInside
                {
                    break
                }
            }
        }
        return isInside
    }
    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        displayPriority = .defaultHigh
        collisionMode = .circle
        centerOffset = CGPoint(x: 0, y: -10) // Offset center point to animate better with marker annotations
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var annotation: MKAnnotation? {
        willSet {

            canShowCallout = false

            if let cluster = newValue as? UserAnnotation {
                let renderer = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40))
                let count = cluster.memberAnnotations.count
                let uniCount = cluster.memberAnnotations.filter { member -> Bool in
                    //Log("Bool  \(member) , \(member.isMapVisibility == false) 💚")

                    return member.isMapVisibility == true
                }.count
                //Log("COUNTS \(count) , \(uniCount) ❤️")
                image = renderer.image { _ in
                    // Fill full circle with tricycle color
                    if uniCount > 0 {

                        AppTheme.blueColor.setFill()
                        UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 40, height: 40)).fill()

                        // Fill inner circle with white color
                        UIColor.white.setFill()
                        UIBezierPath(ovalIn: CGRect(x: 8, y: 8, width: 24, height: 24)).fill()

                        // Finally draw count text vertically and horizontally centered
                        let attributes = [ NSAttributedString.Key.foregroundColor: UIColor.black,
                                           NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 20)]
                        //let text = "\(count)"
                        let text = "4"
                        let size = text.size(withAttributes: attributes)
                        let rect = CGRect(x: 20 - size.width / 2, y: 20 - size.height / 2, width: size.width, height: size.height)
                        text.draw(in: rect, withAttributes: attributes)
                    }
                }
            }
        }
    }

}

And these are some of my MapKit Functions

extension FeedsViewController: MKMapViewDelegate {

    //   1
      func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard let annotation = annotation as? UserAnnotation else { return nil }
        // 2
        let identifier = "marker"
        if #available(iOS 11.0, *) {
            var view: ClusterView

            if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
                as? ClusterView { // 3
                dequeuedView.annotation = annotation
                view = dequeuedView
            } else {
                // 4
                view = ClusterView(annotation: annotation, reuseIdentifier: identifier)
            }
            return view
        } else {
            // Fallback on earlier versions

            return nil
        }

      }
    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
//        let zoomWidth = mapView.visibleMapRect.size.width
//        let zoomFactor = Int(log2(zoomWidth))
//        print("...REGION DID CHANGE: ZOOM FACTOR \(zoomFactor)")
        let centralLocation = CLLocation(latitude: mapView.centerCoordinate.latitude, longitude:  mapView.centerCoordinate.longitude)

        Log("💚 Radius - \(self.getRadius(centralLocation: centralLocation))")


    }
    func getRadius(centralLocation: CLLocation) -> Double{
        let topCentralLat:Double = centralLocation.coordinate.latitude -  mapView.region.span.latitudeDelta/2
        let topCentralLocation = CLLocation(latitude: topCentralLat, longitude: centralLocation.coordinate.longitude)
        let radius = centralLocation.distance(from: topCentralLocation)
        return radius / 1000.0 // to convert radius to meters
    }
    func mapView(_ mapView: MKMapView,
                 didSelect view: MKAnnotationView)
    {
        // 1
        if view.annotation is MKUserLocation
        {
            // Don't proceed with custom callout
            return
        }
        // 2
        let annotation = view.annotation as! UserAnnotation

        let detailAnnotationView: UserDetailAnnotationView = UserDetailAnnotationView(frame: CGRect(x: 0, y: 0, width: 320, height: 74))

        let url = (annotation.discipline == "") ? nil : URL(string: annotation.discipline)!
        let range = 0.0..<0.9
        if annotation.userProfile.location.dist != nil {
            if range.contains(annotation.userProfile.location.dist) {
                let kMeters = Measurement(value: annotation.userProfile.location.dist, unit: UnitLength.kilometers)
                let meters = kMeters.converted(to: UnitLength.meters)
                detailAnnotationView.distancelbl.text = "\(String(describing: round(Double(meters.value)))) m Away"

            } else {
                detailAnnotationView.distancelbl.text = "\(String(describing: round(annotation.userProfile.location.dist))) Km Away"
            }
        }

        detailAnnotationView.set(Title: annotation.title!, imageUrl: url) { [weak self] (sender) in
            guard let self = self else { return }
            if self.isOpenChat {

                self.isOpenChat = false

                if annotation.userProfile.channel != "" {

                    self.appDelegate.pubNubAddPushNotifications([annotation.userProfile.channel]) { (status) in

                        print(status.description)
                    }

                    let chatViewController: ChatViewController = self.storyboard?.instantiateViewController(withIdentifier: "ChatViewController") as! ChatViewController

                    chatViewController.userProfile = annotation.userProfile
                    chatViewController.loginUserProfile = self.loginUserProfile
                    self.navigationController?.pushViewController(chatViewController, animated: true)
                }
            }

        }

        detailAnnotationView.center = CGPoint(x: view.bounds.size.width / 2, y: -detailAnnotationView.bounds.size.height*0.52)

        view.addSubview(detailAnnotationView)

        mapView.setCenter((view.annotation?.coordinate)!, animated: true)

//        let calloutView = views?[0] as! CustomCalloutView
//        calloutView.starbucksName.text = starbucksAnnotation.name
//        calloutView.starbucksAddress.text = starbucksAnnotation.address
//        calloutView.starbucksPhone.text = starbucksAnnotation.phone
//        calloutView.starbucksImage.image = starbucksAnnotation.image
//        let button = UIButton(frame: calloutView.starbucksPhone.frame)
//        button.addTarget(self, action: #selector(ViewController.callPhoneNumber(sender:)), for: .touchUpInside)
//        calloutView.addSubview(button)
//        // 3
//        calloutView.center = CGPoint(x: view.bounds.size.width / 2, y: -calloutView.bounds.size.height*0.52)
//        view.addSubview(calloutView)
//        mapView.setCenter((view.annotation?.coordinate)!, animated: true)
    }

    func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
        if #available(iOS 11.0, *) {
            if view.isKind(of: ClusterView.self)
            {
                for subview in view.subviews
                {
                    subview.removeFromSuperview()
                }
            }
        } else {
            // Fallback on earlier versions
        }
    }

    func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView,
                 calloutAccessoryControlTapped control: UIControl) {
//        let location = view.annotation as! UserAnnotation
//        let launchOptions = [MKLaunchOptionsDirectionsModeKey:
//            MKLaunchOptionsDirectionsModeDriving]
//        location.mapItem().openInMaps(launchOptions: launchOptions)
    }
}

This is how I am setting up mapView....

fileprivate func setupMapsLayout() {

    if self.userAnnotationList.count > 0 {
    // HereMap
        self.mapView.removeAnnotations(self.userAnnotationList)
    }

    self.mapView.delegate = self
    //    mapView.register(ArtworkMarkerView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
    if #available(iOS 11.0, *) {
        // HereMap
        //self.mapView.register(UserAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
        mapView.register(ClusterView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
    } else {
        // Fallback on earlier versions
    }

    self.loadInitialData()

    self.mapView.addAnnotations(self.userAnnotationList)
    //self.mapView.topCenterCoordinate()
}
Mcinnis answered 26/5, 2019 at 8:19 Comment(2)
yes I have added this for another View again, i think if just make a single annotation and the other one which I have used before will cluster the annotations self.mapView.register(ClusterView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)Mcinnis
Unfortunately I am totally new to MkMapView I what i did i have very less idea about that, any guidance to achieve my goal would be helpful and appreciated.Mcinnis
H
17

OK, the iOS 11 and later solution is fairly simple. You have two annotation views, one for your own annotations, and one for clusters of annotations. Your main annotation view simply has to specify the clusteringIdentifier when it’s initialized and when the annotation property changes:

class UserAnnotationView: MKMarkerAnnotationView {
    static let preferredClusteringIdentifier = Bundle.main.bundleIdentifier! + ".UserAnnotationView"

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        clusteringIdentifier = UserAnnotationView.preferredClusteringIdentifier
        collisionMode = .circle
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var annotation: MKAnnotation? {
        willSet {
            clusteringIdentifier = UserAnnotationView.preferredClusteringIdentifier
        }
    }
}

And your cluster annotation view should just update its image when its annotation property is updated:

class UserClusterAnnotationView: MKAnnotationView {
    static let preferredClusteringIdentifier = Bundle.main.bundleIdentifier! + ".UserClusterAnnotationView"

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        collisionMode = .circle
        updateImage()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var annotation: MKAnnotation? { didSet { updateImage() } }

    private func updateImage() {
        if let clusterAnnotation = annotation as? MKClusterAnnotation {
            self.image = image(count: clusterAnnotation.memberAnnotations.count)
        } else {
            self.image = image(count: 1)
        }
    }

    func image(count: Int) -> UIImage {
        let bounds = CGRect(origin: .zero, size: CGSize(width: 40, height: 40))

        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { _ in
            // Fill full circle with tricycle color
            AppTheme.blueColor.setFill()
            UIBezierPath(ovalIn: bounds).fill()

            // Fill inner circle with white color
            UIColor.white.setFill()
            UIBezierPath(ovalIn: bounds.insetBy(dx: 8, dy: 8)).fill()

            // Finally draw count text vertically and horizontally centered
            let attributes: [NSAttributedString.Key: Any] = [
                .foregroundColor: UIColor.black,
                .font: UIFont.boldSystemFont(ofSize: 20)
            ]

            let text = "\(count)"
            let size = text.size(withAttributes: attributes)
            let origin = CGPoint(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2)
            let rect = CGRect(origin: origin, size: size)
            text.draw(in: rect, withAttributes: attributes)
        }
    }
}

Then, all you have to do is register your classes:

mapView.register(UserAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
mapView.register(UserClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)

No mapView(_:viewFor:) implementation is needed (nor desired). But the above yields (showing the default animation as you zoom out and back in):

enter image description here

Now, clearly, you can modify your UserAnnotationView however you want. (Your question didn’t indicate what the standard, single user annotation view would look like). But by setting its clusteringIdentifier and registering a MKMapViewDefaultClusterAnnotationViewReuseIdentifier you get clustering fairly easily in iOS 11 and later.

If you really want to make the cluster annotation views look like the standard annotation views, you can register the same annotation view class for both:

mapView.register(UserClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
mapView.register(UserClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)

But you then have to give the cluster annotation view the same clusteringIdentifier that we previously gave to the standard annotation view:

class UserClusterAnnotationView: MKAnnotationView {
    static let preferredClusteringIdentifier = Bundle.main.bundleIdentifier! + ".UserClusterAnnotationView"

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        clusteringIdentifier = UserClusterAnnotationView.preferredClusteringIdentifier
        collisionMode = .circle
        updateImage()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var annotation: MKAnnotation? {
        didSet {
            clusteringIdentifier = UserClusterAnnotationView.preferredClusteringIdentifier
            updateImage()
        }
    }

    private func updateImage() {
        if let clusterAnnotation = annotation as? MKClusterAnnotation {
            self.image = image(count: clusterAnnotation.memberAnnotations.count)
        } else {
            self.image = image(count: 1)
        }
    }

    func image(count: Int) -> UIImage {
        let bounds = CGRect(origin: .zero, size: CGSize(width: 40, height: 40))

        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { _ in
            // Fill full circle with tricycle color
            AppTheme.blueColor.setFill()
            UIBezierPath(ovalIn: bounds).fill()

            // Fill inner circle with white color
            UIColor.white.setFill()
            UIBezierPath(ovalIn: bounds.insetBy(dx: 8, dy: 8)).fill()

            // Finally draw count text vertically and horizontally centered
            let attributes: [NSAttributedString.Key: Any] = [
                .foregroundColor: UIColor.black,
                .font: UIFont.boldSystemFont(ofSize: 20)
            ]

            let text = "\(count)"
            let size = text.size(withAttributes: attributes)
            let origin = CGPoint(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2)
            let rect = CGRect(origin: origin, size: size)
            text.draw(in: rect, withAttributes: attributes)
        }
    }
}

That yields:

enter image description here

Personally, I think that’s a little confusing, but if that’s what you’re going for, that’s one way to achieve it.


Now, if you really need to support iOS versions prior to 11 and you want clustering, then you’ll have to do all this clustering logic yourself (or find third party library to do it). Apple shows how to do this in WWDC 2011 Visualizing Information Geographically with MapKit. The concept they employ is the notion of dividing the visible map into a grid, and if there are multiple annotations within a particular grid, they remove them and add a single “cluster” annotation. And they illustrate how you might even visually animate the moving of the annotations in and out of the cluster, so the user can understand what's going on as they zoom in and out. It's a nice starting point as you dive into this.

This is non-trivial, so I’d think long and hard about whether I wanted to implement this myself. I’d either abandon iOS versions prior to 11 or find a third-party implementation (and that question you reference has plenty of examples).

Humfrey answered 26/5, 2019 at 23:57 Comment(6)
Thankyou so so much for figuring this out for me and I am really sorry I had broke my internet so couldn't checked early,Mcinnis
Plus how can i change this red icon to some of my own , i didn't see any way to set that. i want to add some of my own image. like I have added before.Mcinnis
Perfect answer @HumfreyInnominate
As Rob mentions, you need to set the clustering identifier again in willSet. Otherwise after you zoom out the number isn't updated!Tetrapody
Thanks Rob, perfect as needed,Malinger
Worked great for me. Thanks RobOvarian

© 2022 - 2024 — McMap. All rights reserved.