Reputation: 125
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
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:
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.
You really should employ dequeue logic for the user location, too.
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.).
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.
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
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