GreatCornholio
GreatCornholio

Reputation: 125

Can I assign different images to every pin in the map?

I'm kinda new with mapkit, but i wonder, can i set different images to every pin on the map. For instance, there are user informations in a dictionary, and instead of the regular pin image there must be their own images. How should i set the viewFor annotation method for the following output.

[{
email = "[email protected]";
id = jqvDgcBoV9Y4sx1BHCmir5k90dr1;
name = User1;
profileImageUrl = "<null>";
}, {
email = "[email protected]";
id = bqvDmcBoV9Y4sx1BqCmirnk90drz;
name = User2;
profileImageUrl = "https://firebasestorage.googleapis.com/v0/";
}, {
email = "[email protected]";
id = axmDgcB5V9m4sx1nHC5ir5kn1dn3;
name = User3;
profileImageUrl = "https://firebasestorage.googleapis.com/v0/";
}]

By the way, i have a function to convert URL to UIImageView, but not UIImage, this is the one of the my big struggles.

My viewForAnnotation delegate for now.

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {

    var annotationView: MKAnnotationView?
    var annotationViewx: MKAnnotationView?
    let annotationIdentifier = "AnnotationIdentifier"

    guard !annotation.isKind(of: MKUserLocation.self) else {
        var annotationViewq: MKAnnotationView?
        annotationViewq = MKAnnotationView(annotation: annotation, reuseIdentifier: "userLocation")
        annotationViewq?.image = UIImage(named: "myLocation.png")


        let size = CGSize(width: 17, height: 17)
        UIGraphicsBeginImageContext(size)
        annotationViewq?.image!.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))

        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        annotationViewq?.image = resizedImage
        annotationViewq?.isEnabled = true
        annotationViewq?.isUserInteractionEnabled = true
        return annotationViewq

    }


    if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: annotationIdentifier) {
        annotationView = dequeuedAnnotationView
        annotationView?.annotation = annotation
        annotationView?.canShowCallout = true
        annotationView?.image = UIImage(named: "emptyPhoto.png")

        let size = CGSize(width: 17, height: 17)
        UIGraphicsBeginImageContext(size)
        annotationView?.image!.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))

        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        annotationView?.image = resizedImage

        return annotationView
    }
        //This annotation not working. but not problem
        let av = MKAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
        av.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
        annotationViewx?.canShowCallout = true
        annotationViewx?.image = UIImage(named: "trafficIcon.png")

        let size = CGSize(width: 17, height: 17)
        UIGraphicsBeginImageContext(size)
        annotationViewx?.image!.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))

        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        annotationViewx?.image = resizedImage
        annotationViewx = av


    return annotationViewx

}

Upvotes: 0

Views: 1192

Answers (2)

Rob
Rob

Reputation: 438192

First, let me say that I think that matt has nailed the root of the issue, namely that if you have annotations with their own images, you should define your own annotation type that captures the URL of the image, e.g.

class CustomAnnotation: NSObject, MKAnnotation {
    dynamic var coordinate: CLLocationCoordinate2D
    dynamic var title: String?
    dynamic var subtitle: String?

    var imageURL: URL?

    init(coordinate: CLLocationCoordinate2D, title: String? = nil, subtitle: String? = nil, imageURL: URL? = nil) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle
        self.imageURL = imageURL

        super.init()
    }
}

When you add your annotations, make sure to supply the URL, and you're off to the races. The only additional thing I'd point out is that you really want to keep track of which network request is associated with which annotation (so that you can cancel it if you need). So I would add a URLSessionTask property to the annotation view class:

class CustomAnnotationView: MKAnnotationView {
    weak var task: URLSessionTask?    // keep track of this in case we need to cancel it when the annotation view is re-used
}

Frankly, I’d pull all of your complicated configuration code out of the mapView(_:viewFor:) method and put it in the annotation view classes, for a better division of labor and avoiding view controller bloat.

So, for example, a custom annotation view for the MKUserLocation annotation:

class CustomUserAnnotationView: MKAnnotationView {
    static let reuseIdentifier = Bundle.main.bundleIdentifier! + ".customUserAnnotationView"
    private let size = CGSize(width: 17, height: 17)

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

        image = UIImage(named: "myLocation.png")?.resized(to: size)
    }

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

And another for your CustomAnnotation annotation with imageURL property, where it will asynchronously fetch the desired image:

class CustomAnnotationView: MKAnnotationView {
    static let reuseIdentifier = Bundle.main.bundleIdentifier! + ".customAnnotationView"

    private weak var task: URLSessionTask?
    private let size = CGSize(width: 17, height: 17)

    override var annotation: MKAnnotation? {
        didSet {
            if annotation === oldValue { return }

            task?.cancel()
            image = UIImage(named: "emptyPhoto.png")?.resized(to: size)
            updateImage(for: annotation)
        }
    }

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

        canShowCallout = true
        rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
        image = UIImage(named: "emptyPhoto.png")?.resized(to: size)
    }

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

    private func updateImage(for annotation: MKAnnotation?) {
        guard let annotation = annotation as? CustomAnnotation, let url = annotation.imageURL else { return }

        let task = URLSession.shared.dataTask(with: url) { data, _, error in
            guard let data = data,
                let image = UIImage(data: data)?.resized(to: self.size),
                error == nil else {
                    print(error ?? "Unknown error")
                    return
            }

            DispatchQueue.main.async {
                UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve, animations: {
                    self.image = image
                }, completion: nil)
            }
        }
        task.resume()
        self.task = task
    }
}

Then, in iOS 11 and later, my view controller can simply register these two classes in viewDidLoad:

mapView.register(CustomUserAnnotationView.self, forAnnotationViewWithReuseIdentifier: CustomUserAnnotationView.reuseIdentifier)
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: CustomAnnotationView.reuseIdentifier)

And then, the mapView(_:viewFor:) distills down to a much simpler method:

extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        let identifier: String

        switch annotation {
        case is MKUserLocation:   identifier = CustomUserAnnotationView.reuseIdentifier
        case is CustomAnnotation: identifier = CustomAnnotationView.reuseIdentifier
        default:                  return nil
        }

        return mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: annotation)
    }
}

Note, I've tried to fix a whole bunch of other issues buried in your viewForAnnotation method, notably:

  1. Your image resizing logic (a) was repeated a couple of times; and (b) didn't call UIGraphicsEndImageContext. So, I'd suggest pulling that out (for example in this UIImage extension) and might as well use UIGraphicsImageRenderer to simplify it:

    extension UIImage {
        func resized(to size: CGSize) -> UIImage {
            return UIGraphicsImageRenderer(size: size).image { _ in
                draw(in: CGRect(origin: .zero, size: size))
            }
        }
    }
    

    You might want to consider whether you want aspect fill or something like that, but here are a few other permutations of the idea: https://stackoverflow.com/a/28513086/1271826. Or, perhaps better, take a look at the resizing routines of AlamofireImage or Kingfisher.

    But the take home message is that you should pull the gory resizing logic out of viewForAnnotation and into its own routine/library.

  2. You really should employ dequeue logic for the user location, too.

  3. Note, I'm just doing simple URLSession.shared.dataTask without looking for caches, etc. You obviously can get more sophisticated here (e.g. caching the resized image views ,etc.).

  4. It's not a big deal, but I find this construct to be a bit unwieldy:

    guard !annotation.isKind(of: MKUserLocation.self) else { ... }
    

    So I simplified that to use is test. E.g.:

    if annotation is MKUserLocation { ... }
    

    It's largely the same thing, but a bit more intuitive.

  5. Note, the above routine, like yours, uses a resized placeholder image for the annotation view's image property. For the sake of future readers, let me say that this is important, because the standard annotation view doesn't gracefully handle asynchronous changes in size of the image. You can either modify that class to do so, or, easier, like we have here, use a standard sized placeholder image.

Note, see the prior revision of this answer for iOS 10.x and earlier.

Upvotes: 2

matt
matt

Reputation: 535890

but i wonder, can i set different images to every pin on the map

Certainly. You need to create a custom annotation type. Your custom annotation type will carry the image information in an instance property that you will give it. That way, when you get to mapView(_:viewFor:), you will know what image this annotation needs and can assign it.

Example:

class MyAnnotation : NSObject, MKAnnotation {
    dynamic var coordinate : CLLocationCoordinate2D
    var title: String?
    var subtitle: String?
    var imageName: String?

    init(location coord:CLLocationCoordinate2D) {
        self.coordinate = coord
        super.init()
    }
}

When you create the annotation, assign to its imageName as well, before you attach it to the map. Now the map calls the delegate asking for an image view, you can read the imageName property and you will know what to do.

Upvotes: 1

Related Questions