Swift - MapView intermittently shows annotation title/subtitle as blank - swift

My iOS map view shows an initial annotation. I want the user to be able to click on the map and for the annotation to move to the spot where the user clicked, but to retain the non-coordinate information in the annotation (e.g. title, subtitle). The code below works, but intermittently the title and subtitle are not showing (see animation and that the name "Spot Name" sometimes doesn't show). It's unclear to me why this is occurring. If I add print statements to print spot.name and spot.title after mapView.addAnnotation, the String values are there & remain unchanged in the MKAnnotation conforming class. Also, when I click in the marker, the proper title and subtitle show in the callout, even if they weren't showing in the annotation. Grateful for any advice/corrections. Thanks!
import UIKit
import MapKit
class SpotDetailViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
var spot: Spot! // Spot class conforms to NSObject & MKAnnotation
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
spot = Spot()
spot.coordinate = CLLocationCoordinate2D(latitude: 42.334709, longitude: -71.170061)
spot.name = "Spot Name"
spot.address = "Spot Address, Spot Town, Spot State"
// Set initial region
let regionDistance: CLLocationDistance = 250
let region = MKCoordinateRegionMakeWithDistance(spot.coordinate, regionDistance, regionDistance)
mapView.setRegion(region, animated: true)
mapView.addAnnotation(self.spot)
}
#IBAction func mapViewTapped(_ sender: UITapGestureRecognizer) {
let annotationView = mapView.view(for: mapView.annotations[0])
let touchPoint = sender.location(in: mapView)
guard !(annotationView?.frame.contains(touchPoint))! else {
return
}
let newCoordinate: CLLocationCoordinate2D = mapView.convert(touchPoint, toCoordinateFrom: mapView)
spot.coordinate = newCoordinate
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotation(self.spot)
mapView.setCenter(spot.coordinate, animated: true)
}
}
extension SpotDetailViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let identifer = "Marker"
var view: MKMarkerAnnotationView
if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifer) as? MKMarkerAnnotationView {
dequeuedView.annotation = annotation
view = dequeuedView
} else {
view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifer)
view.canShowCallout = true
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
}
return view
}
}

Related

How can I use the generic Apple Maps pins for locations in MapKit, rather than having to use my own images as pins?

In my application, I have stored coordinates in FireBase and am displaying those coordinates on a Map View in the code below. The current location of the user is also being harnessed. I would like the locations I have stored in FireBase to be represented by the generic Apple Maps pins, but I do not know how to get the app to work unless I use custom images as pins instead. Also, the generic "blue dot" of the user displayed on the map to represent the current location will not appear for me. In different projects in which the locations are stored in-app rather than in FireBase, the generic pins and blue dot appear just as I would like them to. How can I fix this?
I have tried changing bits and pieces of the code, but I am rather new to Swift and the programming world in general, and cannot find a solution.
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
#IBAction func mapSwitch(_ sender: UISwitch) {
if (sender.isOn == true) {
mapView.mapType = MKMapType.standard
}
else {
mapView.mapType = MKMapType.hybrid
}
}
var tappedAnnotation : MKAnnotation?
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let view = mapView.dequeueReusableAnnotationView(withIdentifier: "Annotation") {
view.annotation = annotation
view.image = UIImage(named: "pin")
return view
} else {
let view = MKAnnotationView(annotation: annotation, reuseIdentifier: "Annotation")
view.image = UIImage(named: "pin")
view.isEnabled = true
view.canShowCallout = true
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
return view
}
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
tappedAnnotation = view.annotation
performSegue(withIdentifier: "showAnnotationDetails", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showAnnotationDetails", let dest = segue.destination as? AnnotationDetails {
dest.annotation = tappedAnnotation
}
}
func createAnnotations ( _ annotations : [String:[String:Any]] ) {
mapView.removeAnnotations(mapView.annotations)
for (_,values) in annotations {
if let latDouble = values["latitude"] as? Double, let longDouble = values["longitude"] as? Double {
let lat = CLLocationDegrees( latDouble )
let long = CLLocationDegrees( longDouble )
let coord = CLLocationCoordinate2D(latitude: lat, longitude: long)
let annotation = MKPointAnnotation()
annotation.coordinate = coord
annotation.title = values["name"] as? String ?? ""
annotation.subtitle = values["info"] as? String ?? ""
mapView.addAnnotation(annotation)
}
}
}
let manager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
mapView.delegate = self
mapView.mapType = MKMapType.standard
let ref = Database.database().reference()
ref.child("annotations").observe(.value) { snapshot in
print(snapshot)
if let annotations = snapshot.value as? [String:[String:Any]] {
self.createAnnotations(annotations)
} else {
print("Data received not formatted as expected")
}
}
}
Use MKPinAnnotationView instead of MKAnnotationView
Set mapView.showsUserLocation = true to show user location (blue dot)

MKPinAnnotationView doesn't show in MKMapView even though it's properly added using Swift

I'm developing an OSX app.
I've subclassed a MKAnnotation:
import Foundation
import MapKit
class LocationAnnotation: NSObject, MKAnnotation {
var image: NSImage?
var title: String?
var coordinate: CLLocationCoordinate2D
init(image anImage: NSImage?, title aTitle: String, coordinate aCoordinate: CLLocationCoordinate2D) {
self.image = anImage
self.title = aTitle
self.coordinate = aCoordinate
}
}
In my NSViewController subclass, I've added a MKMapViewDelegate and in Interface Builder I've added a MKMapView and set its delegate to NSViewController.
To find out what's wrong, I'm adding three locations in my ViewDidLoad method, into an array. I'm adding those annotations to my map in ViewDidAppear. I plan to move that to a background thread when I figure out what's wrong.
import Cocoa
import MapKit
class ShowLocationsViewController: NSViewController, MKMapViewDelegate {
#IBOutlet var locationMap: MKMapView!
private var myLocationArray: [LocationAnnotation] = []
private var myRegion: MKCoordinateRegion!
//#MARK: - UIViewController
override func viewDidLoad() {
super.viewDidLoad()
myLocationArray = []
myRegion = nil
let locLondon = LocationAnnotation(image: nil, title: "London", coordinate: CLLocationCoordinate2DMake(51.522617, -0.139371))
let locWembley = LocationAnnotation(image: nil, title: "Wembley", coordinate: CLLocationCoordinate2DMake(51.555909, -0.279600))
let locGreenwich = LocationAnnotation(image: nil, title: "Greenwich", coordinate: CLLocationCoordinate2DMake(51.476572, -0.001596))
myLocationArray.append(locLondon)
myLocationArray.append(locWembley)
myLocationArray.append(locGreenwich)
myRegion = MKCoordinateRegion.init(center: locLondon.coordinate, span: MKCoordinateSpanMake(0.20, 0.20)) // 20km span
}
override func viewDidAppear() {
locationMap.addAnnotations(myLocationArray)
locationMap.setRegion(myRegion, animated: true)
}
}
When using delegate's method viewFor annotation: I print out the title of each annotation in a log and all three of them are listed. In another delegate's method didAdd I'm printing out the number of annotations of my map in a log and it prints out three. But on my map, there are no annotations displayed. I tried panning and zooming with no success. Region gets properly set and displayed though.
//#MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
print(locationMap.annotations.count, views.count)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is LocationAnnotation {
let annotationIdentifier = "Location"
var annotationView = locationMap.dequeueReusableAnnotationView(withIdentifier: annotationIdentifier) as! MKPinAnnotationView?
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
}
annotationView!.canShowCallout = true
print(annotationView!.annotation!.title ?? "no view")
return annotationView
} else {
print("no annotation")
return nil
}
}
When I try the same code in an iOS app, everything works. I assume there is nothing wrong with my delegate because methods are properly called.
I would appreciate any help you can give me.
I figured out the problem, the way the window is being called.
I created a variable mainWindowController in my AppDelegate, then I subclassed my main NSViewController so that I could hook up AppDelegate to it.
let appDelegate = NSApp.delegate as! AppDelegate
appDelegate.mainWindowController = self
Afterwards I used this code to call my window which was errornous:
let mapViewController = mainWindowController?.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier.init(rawValue: "ShowLocationsID")) as! ShowLocationsViewController
mainWindowController?.contentViewController?.presentViewControllerAsModalWindow(mapViewController)
I changed that in a way that I created a segue in my storyboard and called the following code which worked and my pins did show:
performSegue(withIdentifier: NSStoryboardSegue.Identifier(rawValue: "ShowLocations"), sender: self)

Make mutliple pin annotations buttons to segue to an other view controller (swift 3)

I have two pin annotations on my map. I created a info button to perform the segue. But the button appears only on one annotation. Can anyone help me please? For the annotations I created a function that I put into the viewdidload method.
Here`s the code:
import UIKit
import MapKit
class MapVC: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
var locManager: CLLocationManager!
override func viewDidLoad() {
super.viewDidLoad()
meinkartenPunkt1()
meinkartenPunkt2()
locManager = CLLocationManager()
locManager.requestWhenInUseAuthorization()
// Do any additional setup after loading the view.
mapView.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
func mapView(_ mapView: MKMapView,didUpdate userLocation: MKUserLocation){
mapView.region.center=userLocation.coordinate
mapView.showAnnotations(mapView.annotations, animated: true)
}
func meinkartenPunkt1() {
let breite: CLLocationDegrees = 8.737653
let länge: CLLocationDegrees = 47.504333
let Koordinaten = CLLocationCoordinate2DMake(länge, breite)
let Span = MKCoordinateSpanMake(0.01, 0.01)
let Region = MKCoordinateRegionMake(Koordinaten, Span)
mapView.setRegion(Region, animated: true)
let Stecknadel = MKPointAnnotation()
Stecknadel.coordinate = Koordinaten
Stecknadel.title = "Heiligberg"
mapView.addAnnotation(Stecknadel)
}
func meinkartenPunkt2() {
let breite: CLLocationDegrees = 8.734345
let länge: CLLocationDegrees = 47.508456
let Koordinaten = CLLocationCoordinate2DMake(länge, breite)
let Span = MKCoordinateSpanMake(0.01, 0.01)
let Region = MKCoordinateRegionMake(Koordinaten, Span)
mapView.setRegion(Region, animated: true)
let Stecknadel = MKPointAnnotation()
Stecknadel.coordinate = Koordinaten
Stecknadel.title = "Kanti Rychenberg"
mapView.addAnnotation(Stecknadel)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
if annotation is MKUserLocation {return nil}
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
pinView!.canShowCallout = true
pinView!.animatesDrop = true
let calloutButton = UIButton(type: .detailDisclosure)
pinView!.rightCalloutAccessoryView = calloutButton
pinView!.sizeToFit()
}
else {
pinView!.annotation = annotation
}
return pinView
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView,
calloutAccessoryControlTapped control: UIControl) {
if control == view.rightCalloutAccessoryView {
performSegue(withIdentifier: "bookDetails", sender: self)
}
}
}
When looking at your source code, I cannot directly see an answer, however I see an immediate problem. You do some important setup on the pinView, while it is nil. To be honest, this code certainly crashes, if it is ever reached:
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
pinView!.canShowCallout = true
Luckily for you, pinView never is nil however, as the mapView creates the pinView successfully. What you probably want to do, is to set the calloutButton, if it is not set. So instead of if pinView == nil you write:
if pinView?.rightCalloutAccessoryView == nil {
// Initialize the rightCalloutAccessoryView here
}
Tip: In general you should avoid force unwraps with ! as it leads to crashes. Try to use optional chaining with ? instead.

Custom MKPointAnnotation isn't responding to user interaction

I am making a Swift application that uses MKPointAnnotations, and I recently ran into an issue where I needed to store metadata in my annotations, so I created the custom class below:
class BRETTFAnnotation: MKPointAnnotation {
var tag: Int64
var name: String
init(lat : Double, lon:Double, t : Int64, n: String) {
self.tag = t
self.name = n
super.init()
self.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
}
}
My MKAnnotationView viewfor MKAnnotation method is shown below:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let newAnnotation = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "reuse")
newAnnotation.canShowCallout = true
let right = self.button(title: "Yes")
right?.addTarget(self, action: #selector(clickedToConfirmNewPoint), for: .touchUpInside)
newAnnotation.rightCalloutAccessoryView = right
let left = self.button(title: "No")
left?.addTarget(self, action: #selector(clickedToCancelNewPoint), for: .touchUpInside)
newAnnotation.leftCalloutAccessoryView = left
return newAnnotation
}
The problem I am running into is when ever I click on my custom BRETTFAnnotation (which I add to my MKMapView) nothing happens. When I was just using the MKPointAnnotation (instead of the BRETTFAnnotation) when I clicked on the map the two buttons on the MKAnnotationView would show. I am trying to get the MKPinAnnotationView to show on touch using my BRETTFAnnotation instead of the MKPointAnnotation.
How can I continue to use my custom annotation and show the callout when the user clicks on the annotation at the same time?
Edit 1: Since it is probably useful the code below is how I make the annotation and add it to the mapView.
let location = gestureRecognizer.location(in: mapView)
let coordinate = mapView.convert(location,toCoordinateFrom: mapView)
print("adding lat,long \(coordinate.latitude),\(coordinate.longitude)")
lastPoint = BRETTFAnnotation(lat: coordinate.latitude, lon: coordinate.longitude, t: 1, n: "")
let annotationView = MKPinAnnotationView(annotation: lastPoint, reuseIdentifier: "reuse")
mapView.addAnnotation(lastPoint)
I fix this problem by making my BRETTFAnnotation a subclass of NSObject and MKAnnotation instead of MKPointAnnotation. Doing this allowed my custom class to receive user interaction and show the callouts.
When you use your own MKAnnoation you can handle your actions in didSelect. Just implement the following code.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if let yourAnnotation = view.annotation as? BRETTFAnnotation {
//handle your meta data or/and show UIViews or whatever
}
}
with
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
//getting called when you tap on map or on another annotation (not the selected annotation before)
//hide UIViews or do whatever you want
}
That does work for me:
class ViewController: UIViewController, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
print("didSelect")
if let annoation = view.annotation as? MyAnnoation {
print("metatag \(annoation.metaTag)")
}
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
print("didDeselect")
}
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
let annotation = MyAnnoation(n: "name", m: "metaTag")
annotation.coordinate = CLLocationCoordinate2D(latitude: 50.0, longitude: 8.0)
mapView.addAnnotation(annotation)
}
}
class MyAnnoation: MKPointAnnotation {
var name: String?
var metaTag: String?
init(n: String, m: String) {
self.name = n
self.metaTag = m
}
}

MKAnnotationView Now Showing AccessoryItem

I have tried following different tutorials to make my MKAnnotations have an accessory item that will segue to a different page when clicked; however, I cannot get the item to show. Here is the code I've come up with after reviewing different sources:
extension MapController : MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let identifier = "PlaceAnnotation"
if annotation is PlaceAnnotation {
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView!.canShowCallout = true
} else {
annotationView!.annotation = annotation
}
annotationView!.leftCalloutAccessoryView = nil
annotationView!.rightCalloutAccessoryView = UIButton(type: UIButtonType.detailDisclosure)
return annotationView
}
return nil
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
let place = view.annotation as! PlaceAnnotation
let placeName = place.title
print(placeName!)
let placeInfo = place.placeObject
let ac = UIAlertController(title: placeInfo?.title, message: placeInfo?.description, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
}
}
Then, to create the annotation, I have this:
for place in places {
let placeCoord = CLLocationCoordinate2D(latitude: CLLocationDegrees(place.latitude)!, longitude: CLLocationDegrees(place.longitude)!)
let annotation = PlaceAnnotation(title: place.title, coordinate: placeCoord, placeObject: place)
mapView.addAnnotation(annotation)
}
And the PlaceAnnotation class is as follows:
class PlaceAnnotation: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var placeObject: Location?
init(title: String, coordinate: CLLocationCoordinate2D, placeObject: Location) {
self.title = title
self.coordinate = coordinate
self.placeObject = placeObject
}
}
The only thing that displays on the annotation when clicked is the Title, but nothing else. I appreciate any help, thank you very much!
(I am working in Swift 3)
I tried your code and it works perfectly fine for me.
But when I removed say,
mapView.delegate = self
Accessory item did not show up, because the delegate is not called.