Problem with reusable MKAnnotationView to show distinct images - ios

I am showing 3 distinct annotations in a map. To achive this I have a enum as a class variable which indicates the current value of the image name to be set in the MKAnnotationView property. I have subclassed MKAnnotationView to sore a class variable to get the image name in case of annotation reuse.
The problem is that when I drag the map leaving the annotations out of view and when I drag it again to see the annotations, these have their images exchanged.
My enum and custom MKAnnotationView class:
enum AnnotationIcon: String {
case taxi
case takingIcon
case destinyIcon
}
final class MyMKAnnotationView: MKAnnotationView {
var iconType: String = ""
}
And this is my viewFor annotation function:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation) else {
return nil
}
let identifier = "Custom annotation"
var annotationView: MyMKAnnotationView?
guard let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MyMKAnnotationView else {
let av = MyMKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView = av
annotationView?.annotation = annotation
annotationView?.canShowCallout = true
annotationView?.translatesAutoresizingMaskIntoConstraints = false
annotationView?.widthAnchor.constraint(equalToConstant: 35).isActive = true
annotationView?.heightAnchor.constraint(equalToConstant: 35).isActive = true
annotationView?.iconType = annotationIcon.rawValue //AnnotationIcon enum class variable
annotationView?.image = UIImage(named: annotationView?.iconType ?? "")
return av
}
annotationView = dequeuedAnnotationView
annotationView?.image = UIImage(named: annotationView?.iconType ?? "")
return annotationView
}
Images that explain the problem:
Before the draggin:
After the draggin:
What is the way for each annotation to retrieve the correct image in case of reuse?
Thank you.

Before reusing the MyMKAnnotationView you've to empty the already set image in the prepareForReuse method.
final class MyMKAnnotationView: MKAnnotationView {
var iconType: String = ""
//...
override func prepareForReuse() {
super.prepareForReuse()
image = nil // or set a default placeholder image instead
}
}
Update: As suspected the iconType is not getting set before you're trying to set the image of MyMKAnnotationView. Either you need to set the iconType before setting the image, like this:
annotationView?.iconType = AnnotationIcon.taxi.rawValue
annotationView?.image = UIImage(named: annotationView?.iconType ?? "")
You can improve this a lot by being the image returning logic to the AnnotationIcon.
enum AnnotationIcon: String {
//...
var annotationImage: UIImage? { UIImage(named: rawValue) }
}
Then change the MyMKAnnotationView as follows:
final class MyMKAnnotationView: MKAnnotationView {
var iconType = AnnotationIcon.taxi {
didSet {
image = iconType.annotationImage // image is set whenever `iconType` is set
}
}
//...
}
Then in viewForAnnotation:
var iconTypes = [AnnotationIcon]() // should be equal to the number of annotation
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
//...
dequeuedAnnotationView.annotation = annotation
dequeuedAnnotationView.iconType = iconType
}

Your problem is that you only set the iconType property when you create the annotation view. When an annotation view is reused, you set the image based on that property rather the current annotation.
Really, there is no need for the iconType property. You should just always use an icon value from your annotation. The annotation is the data model.
You also don't set the view's annotation property correctly in the case of reuse.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let myAnnotation = annotation as? MyAnnotation else {
return nil
}
let identifier = "Custom annotation"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MyMKAnnotationView
if annotationView == nil {
annotationView = MyMKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.canShowCallout = true
annotationView?.translatesAutoresizingMaskIntoConstraints = false
annotationView?.widthAnchor.constraint(equalToConstant: 35).isActive = true
annotationView?.heightAnchor.constraint(equalToConstant: 35).isActive = true
}
annotationView?.annotation = MyAnnotation
annotationView?.image = UIImage(named: MyAnnotation.icon.rawValue)
return annotationView
}

I have solved this issue subclassing the MKPointAnnotation to know what type of Annotation I am reusing:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation) else {
return nil
}
var annotationView: MKAnnotationView?
switch annotation {
case is MyCustomTaxiAnnotation:
annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: annotationTaxiID, for: annotation)
case is MyCustomOriginAnnotation:
annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: annotationOriginID, for: annotation)
case is MyCustomDestinyAnnotation:
annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: annotationDestinyID, for: annotation)
default:
annotationView = nil
}
if annotationView == nil {
switch annotationIcon {
case .destinyIcon:
annotationView = MyDestinyView(annotation: annotation, reuseIdentifier: annotationDestinyID)
annotationView?.annotation = MyCustomDestinyAnnotation()
case .takingIcon:
annotationView = MyOriginView(annotation: annotation, reuseIdentifier: annotationOriginID)
annotationView?.annotation = MyCustomOriginAnnotation()
case .taxi:
annotationView = MyTaxiView(annotation: annotation, reuseIdentifier: annotationTaxiID)
annotationView?.annotation = MyCustomTaxiAnnotation()
}
} else {
annotationView?.annotation = annotation
}
return annotationView
}
For example, the Taxi annotation is a subclass of MKPointAnnotation:
let annotation = MyCustomTaxiAnnotation()
final class MyCustomTaxiAnnotation: MKPointAnnotation {
///...
}
So, taking in account that, I am able to reuse a custom annotation view properly. I have also register the custom MKAnnotationView:
map.register(MyOriginView.self, forAnnotationViewWithReuseIdentifier: annotationOriginID)
map.register(MyDestinyView.self, forAnnotationViewWithReuseIdentifier: annotationDestinyID)
map.register(MyTaxiView.self, forAnnotationViewWithReuseIdentifier: annotationTaxiID)

Related

Using MKLocalSearch in Swift attaches a pin to the users location instead of the blue dot

I am new to coding and Stack Overflow, so forgive me if I do something wrong.
I am using MKLocalSearch to display locations specified by a string. I have a user and location, so everything is setup.
I have added MKLocalSearch to my app, and it works correctly but now puts an MKPointAnnotation over the users' location. Of course, I want the famous blue dot to appear rather than an annotation.
I have already tried going over the code and to look up this issue but haven't had any luck finding a solution.
Here is my MKLocalSearch Code:
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "Dispensaries"
request.region = MapView.region
let search = MKLocalSearch(request: request)
search.start(completionHandler: {(response, error) in
if error != nil {
print("Error occured in search")
} else if response!.mapItems.count == 0 {
print("No matches found")
} else {
print("Matches found")
for item in response!.mapItems {
let annotation = MKPointAnnotation()
annotation.title = item.name
annotation.coordinate = item.placemark.coordinate
DispatchQueue.main.async {
self.MapView.addAnnotation(annotation)
}
print("Name = \(String(describing: item.name))")
print("Phone = \(String(describing: item.phoneNumber))")
print("Website = \(String(describing: item.url))")
}
}
})
Here is my viewForAnnotation
extension MapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
var view = mapView.dequeueReusableAnnotationView(withIdentifier: "reuseIdentifier") as? MKMarkerAnnotationView
if view == nil {
view = MKMarkerAnnotationView(annotation: nil, reuseIdentifier: "reuseIdentifier")`
let identifier = "hold"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.canShowCallout = true
let btn = UIButton(type: .detailDisclosure)
annotationView?.rightCalloutAccessoryView = btn
} else {
annotationView?.annotation = annotation
}
}
view?.annotation = annotation
view?.displayPriority = .required
return view
}
}
In func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) check the annotation type e.g.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
// the rest of your code
}
If you return nil from this method, the MKMapView uses its default built in annotation views, for MKPointAnnotation it uses the red pin, for MKUserLocation the blue dot you are looking for.

MKAnnotationView layer is not of expected type: MKLayer

So my code works fine but my logger is riddled with this message. Is there a way to get rid of it or suppress it?
PostAnnotation.swift
class PostAnnotation: MKPointAnnotation {
//MARK: properties
let post: Post
//MARK: initialization
init(post: Post) {
self.post = post
super.init()
self.coordinate = CLLocationCoordinate2D(latitude: post.latitude, longitude: post.longitude)
self.title = post.title
self.subtitle = post.timeString()
}
}
Adding the annotation
let annotation = PostAnnotation(post: post)
self.map.addAnnotation(annotation)
func mapView
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "pin") as? MKPinAnnotationView
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")
} else {
annotationView?.annotation = annotation
}
if let annotation = annotation as? PostAnnotation {
annotationView?.pinTintColor = UIColor.blue
annotationView?.canShowCallout = true
annotationView?.rightCalloutAccessoryView = UIButton(type: .infoLight)
annotationView?.animatesDrop = true
}
return annotationView
}
Removing this function removes the message
This is a bug in iOS 11, since MKLayer is not a public class.
I'd simply ignore the message, but if it's bothering you: To silence this warning, you can set OS_ACTIVITY_MODE=disable in the scheme's environment page. Beware though, you will silence other OS warnings as well.

Adding onClick event to MKAnnotation swift

I am currently getting some locations from a web request using alamofire, store them in my Object array and then display them on a MKMapView.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation) else {
return nil
}
let annotationIdentifier = "AnnotationIdentifier"
var annotationView: MKAnnotationView?
if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: annotationIdentifier) {
annotationView = dequeuedAnnotationView
annotationView?.annotation = annotation
}
else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
}
if let annotationView = annotationView {
annotationView.canShowCallout = true
annotationView.image = UIImage(named: "atm_map_icon")
}
return annotationView
}
And here is where i parse the json, put the result in an array and the loop through the array and add the markers to the map
for var i in 0..<json.count{
let name = json[i]["Name"].string
let address = json[i]["Address"].string
let lat = json[i]["Lat"].double
let lon = json[i]["Lon"].double
let atm = Atm(name: name!,address: address!,lat: lat!,lon: lon!)
self.atmArrayList.append(atm)
}
print(self.atmArrayList.count)
}
for atm in self.atmArrayList{
let atmPin = AtmAnnotation(title: atm.name!, subtitle: atm.address!, atm: atm, coordinate: CLLocationCoordinate2D(latitude: atm.lat!, longitude: atm.lon!))
self.annotationArray.append(atmPin)
}
self.atmMapView.addAnnotations(self.annotationArray)
What i want to achieve is so when the user clicks on the rightCalloutAccessoryView button, i want to pass the specific Atm object of that Annotation to the another ViewController.
My guess is to give the Annotations an id and then get the Atm from the Atm array in that specific position??
You need to implement this method of MapViewDelegate in your viewController
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
let anotherViewController = self.storyboard?.instantiateViewController(withIdentifier: "anotherViewController") as! AnotherViewController
if let atmPin = view.annotation as? AtmAnnotation
{
anotherViewController.currentAtmPin = atmPin
}
self.navigationController?.pushViewController(anotherViewController, animated: true)
}
Hope this helps

Passing data from a query to mapView viewForAnnotation function

I am trying to display custom pins on an MKMapView. These pins will have a custom image as well as a UILabel that will display a value.
I was able to successfully create the custom pin, with label. Right now the label displays a static value. I queried the data from a backend service like parse, and saved the data for each point. That way when the user taps on a certain point i can display the data in the viewController, however I am not sure how to pass this data from my query method into the didSelectAnnotation and viewForAnnotation methods.
I also would like to change the static value that the label shows to one queried from the server. I tried to do this by creating a class called CustomPointAnnotation, which inherits from MKPointAnnotation and has an initializer with three properties. these properties are set during the query, so how can I access these properties in the mapViewDidSelectAnnotationView, and the viewForAnnotation functions so that I can use the data for my needs. (for things like setting the text for a label within the viewController to a property of that specific annotation).
Below is an image that shows the viewController and what I have so far:
Here is the custom point class:
class CustomPointAnnotation: MKPointAnnotation {
var price: String!
var streetName: String!
var ratingValue: Int!
init?(price: String, streetName: String, ratingValue: Int) {
self.price = price
self.streetName = streetName
self.ratingValue = ratingValue
super.init()
}
}
Below is the query that I run in viewDidLoad:
func displayPoints() {
let pointsQuery = PFQuery(className: "testLocation")
let currentLocation = PFGeoPoint(location: locationManager.location)
pointsQuery.whereKey("location", nearGeoPoint: currentLocation, withinMiles: 2)
pointsQuery.findObjectsInBackgroundWithBlock { (points, error) -> Void in
if error == nil {
print("number of spots: \(points?.count)")
let spots = points! as [PFObject]
for pinPoint in spots {
let point = pinPoint["location"] as! PFGeoPoint
let price = String(pinPoint["price"])
let ratingValue = pinPoint["rating"] as! Int
let streetName = "Park Street, San Francisco CA"
self.customAnnotation = CustomPointAnnotation(price: price, streetName: streetName, ratingValue: ratingValue)
//// PRINT DATA OBTAINED FOR TESTING PURPOSES///////////////////////////////////////////////////////////
print(self.customAnnotation.price)
print(self.customAnnotation.streetName)
print(self.customAnnotation.ratingValue)
///////////////////////////////////////////////////////////////////////////////////////////////////////
self.customAnnotation!.coordinate = CLLocationCoordinate2DMake(point.latitude, point.longitude)
self.priceArray.append(pinPoint["price"])
self.customAnnotation!.price = pinPoint["price"] as? String
self.mapView.addAnnotation(self.customAnnotation!)
}
} else {
JSSAlertView().danger(self, title: "something went wrong", text: "error: \(error)")
}
}
}
here is the didSelectAnnotationView:
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
//var anot: MKAnnotation
if ((view.annotation?.isKindOfClass(MKUserLocation)) != nil){
view.image = nil
}
for anot in mapView.annotations {
print(mapView.annotations.count)
let annotationView = mapView.viewForAnnotation(anot)
if (annotationView != nil) {
annotationView?.image = UIImage(named: "pin")
priceLabel.textColor = UIColor.whiteColor()
}
//priceLabel.textColor = UIColor.blueColor()
view.image = UIImage(named: "pinselected")
print("image changed")
}
}
and finally the viewForAnnotation method:
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isKindOfClass(MKUserLocation){
return nil
}
if !(annotation is CustomPointAnnotation) {
print("all custom images added")
return nil
}
let reuseID = "identifier"
var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseID)
if annotationView == nil {
annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: reuseID, price: "13" )
annotationView?.canShowCallout = false
} else {
annotationView?.annotation = annotation
}
//let cpa = annotation as! CustomPointAnnotation
//let annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: nil, price: "11")
//annotationView!.addSubview(priceLabel)
annotationView?.annotation = annotation
annotationView?.image = UIImage(named: "pin.png")
return annotationView
}
You can down cast in swift with the as operator. In didSelectAnnotationView the annotationView has an annotation property. Your custom annotation view will have your custom annotation as its annotation property, so you can attempt to down cast it to your subclass by saying:
if let annotation = view.annotation as? CustomPointAnnotation
Assuming that's possible, you will then have access to your subclass's properties.
func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
//var anot: MKAnnotation
if ((view.annotation?.isKindOfClass(MKUserLocation)) != nil){
view.image = nil
}
for anot in mapView.annotations {
print(mapView.annotations.count)
let annotationView = mapView.viewForAnnotation(anot)
if (annotationView != nil) {
annotationView?.image = UIImage(named: "pin")
priceLabel.textColor = UIColor.whiteColor()
}
//priceLabel.textColor = UIColor.blueColor()
}
view.image = UIImage(named: "pinselected")
if let annotation = view.annotation as? CustomPointAnnotation
{
self.priceLabel.text = annotation.price //for example
//update the rest of your UI
}
print("image changed")
}
Similarly in viewForAnnotation you can down cast the MKAnnotation to CustomPointAnnotation and MKAnnotationView to CustomAnnotationView.
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isKindOfClass(MKUserLocation){
return nil
}
if !(annotation is CustomPointAnnotation) {
print("all custom images added")
return nil
}
let reuseID = "identifier"
let cpa = annotation as! CustomPointAnnotation
var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseID) as! CustomAnnotationView
if annotationView == nil {
annotationView = CustomAnnotationView(annotation: cpa, reuseIdentifier: reuseID, price: cpa.price)
annotationView?.canShowCallout = false
} else {
annotationView?.annotation = cpa
annotationView?.price = cpa.price
}
annotationView?.image = UIImage(named: "pin.png")
return annotationView
}
Your CustomAnnotationView should update its price label when its price is set by implementing price's didSet.

Swift MKPointAnnotation custom Image

I try to create a custom "badget" for my MKPointAnnotation in swift, but it fails as MKPointAnnotation does not have any property like image
var information = MKPointAnnotation()
information.coordinate = location
information.title = "Test Title!"
information.subtitle = "Subtitle"
information.image = UIImage(named: "dot.png") //this is the line whats wrong
Map.addAnnotation(information)
Anyone figured out a swift like solution for that?
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if !(annotation is MKPointAnnotation) {
return nil
}
var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier("demo")
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "demo")
annotationView!.canShowCallout = true
}
else {
annotationView!.annotation = annotation
}
annotationView!.image = UIImage(named: "image")
return annotationView
}

Resources