Swift MKMapViewDelegate - Transferring coordinates between view controllers - swift

I am trying to construct a Map App that can receive user inputs of latitude and longitude coordinates that when entered, will place a pin on a map in a different tab. My FirstVC consists of a button "Add Locations" that segues to OtherVC which the user can input the coordinates. SecondVC consists of the MapView. My initial idea is to have a specific array of coordinates, and any new coordinates will be appended to this array. The execution is where I am lacking, because I am not sure how to transfer this array to the MapView. Here is what I have so far:
For the input of coordinates:
import UIKit
import CoreLocation
class OtherVC: UIViewController {
#IBOutlet weak var latitudeField: UITextField!
#IBOutlet weak var longitudeField: UITextField!
var coordinates = [CLLocationCoordinate2D]()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addToMap(_ sender: Any) {
let lat = Double(latitudeField.text!)
let long = Double(longitudeField.text!)
self.coordinates.append(CLLocationCoordinate2D(latitude: lat!, longitude: long!))
}
}
For the MapView:
import UIKit
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
var coordinates = [CLLocationCoordinate2D]() {
didSet {
// Update the pins
// Since it doesn't check for which coordinates are new, it you go back to
// the first view controller and add more coordinates, the old coordinates
// will get a duplicate set of pins
for (index, coordinate) in self.coordinates.enumerated() {
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotation.title = "Location \(index)"
mapView.addAnnotation(annotation)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let identifier = "pinAnnotation"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.canShowCallout = true
}
annotationView?.annotation = annotation
return annotationView
}
}

I think you need is get your second ViewController MapViewController from your tabBarController and then pass the coordinates array, so in your addToMap Action replace with this
#IBAction func addToMap(_ sender: Any) {
let lat = Double(latitudeField.text!)
let long = Double(longitudeField.text!)
self.coordinates.append(CLLocationCoordinate2D(latitude: lat!, longitude: long!))
//here we pass the coordinate array to mapViewController
if let mapViewController = self.tabBarController?.viewControllers?[1] as? MapViewController
{
mapViewController.coordinates = self.coordinates
}
}
You need also add a navigation controller, like in the picture
I hope this helps you

Generally speaking I only use the method of directly passing the data from one ViewController to the next if there is a parent child relationship and I can do it in prepareForSegue or in and unwind (child to parent). Otherwise I think its better to use the publisher subscriber model with Notification. When your coordinate changes you post a Notification:
NotificationCenter.default.post(name: NSNotification.Name("MapViewController.coordinate.updated"), object: self, userInfo: nil)
Now anyone who cares about MapViewController's coordinates changing can listen:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(coordinateUpdated), name: NSNotification.Name("coordinate.updated"), object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc private func coordinateUpdated( notification: Notification) {
if let source = notification.object as? MapViewController {
print(source.coordinates)
}
}
This makes the views loosely coupled and MapViewController doesn't need to care about who needs to be updated; the subscribers are responsible for registering themselves.

Related

Swift - MapView intermittently shows annotation title/subtitle as blank

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
}
}

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.

Annotations for MKLocalSearch in Swift 3 — Map View Current Location

This is my first time on Stack Overflow. I have been trying to make an app that finds the user's current location and outputs the information the doctors close to the user. So far, I can find the current location, however, I am not able to add an annotation for the doctors near the current location.
Here is my code so far:
import UIKit
import MapKit
import CoreLocation
class ThirdViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var map: MKMapView!
let manager = CLLocationManager()
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let location = locations[0]
let span:MKCoordinateSpan = MKCoordinateSpanMake(0.01, 0.01)
let myLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
let region:MKCoordinateRegion = MKCoordinateRegionMake(myLocation, span)
map.setRegion(region, animated: true)
self.map.showsUserLocation = true
}
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = "doctor"
request.region = map.region
let localSearch:MKLocalSearch = MKLocalSearch(request: request)
localSearch.start(completionHandler: {(result, error) in
for placemark in (result?.mapItems)! {
if(error == nil) {
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2DMake(placemark.placemark.coordinate.latitude, placemark.placemark.coordinate.longitude)
annotation.title = placemark.placemark.name
annotation.subtitle = placemark.placemark.title
self.map.addAnnotation(annotation)
}
else
{
print(error ?? 0)
}
}
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
All responses are appreciated and if you have any advice for what I should do next time I ask a question, please leave it down below. Thank you.
Your map view needs a delegate with a map​View(_:​view​For:​) implementation. Otherwise, adding an annotation will have no visible effect.
To add an annotation you need to inherit MKMapViewDelegate.
Then add: mapView.delegate = self to your initialization method.
And implement this functions:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if !(view.annotation! is MKUserLocation) {
let customPin = view.annotation as! CustomPin //if u wanna use custom pins
configureDetailView(annotationView: view, spotPin: customPin.spotDetailsItem) //configuring view for tap
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}
if !(annotation is CustomPin) {
return nil
}
let identifier = "CustomPin"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.canShowCallout = true
} else {
annotationView!.annotation = annotation
}
return annotationView
}
configureDetailView method and other information you can find here (its is quite cumbersome)

Swift Using NotificationCenter to Pass Data Between View Controllers

I am trying to pass the data of user-input coordinates from one VC to another using Notifications. When I run the code, it does not add an annotation onto the MapView, so I have a hunch I may have not set up the notification to send with the inputted coordinates properly, but I am not sure where I have went wrong.
Class file that takes the input of coordinates:
import UIKit
import CoreLocation
var locations: [Dictionary<String, Any>] = [] // here I initialize my empty array of locations
class OtherVC: UIViewController {
#IBOutlet weak var latitudeField: UITextField!
#IBOutlet weak var longitudeField: UITextField!
#IBOutlet weak var titleTextField: UITextField!
var coordinates = [CLLocationCoordinate2D]()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addToMap(_ sender: Any) {
let lat = latitudeField.text!
let long = longitudeField.text!
let title = titleTextField.text!
var location: [String: Any] = ["title": title, "latitude": lat, "longitude": long]
locations.append(location)
NotificationCenter.default.post(name: NSNotification.Name("MapViewController.coordinate.updated"), object: locations, userInfo: nil)
}
}
Class file that receives notification and places annotation:
import UIKit
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
}
func add(notification: NSNotification) {
let dict = notification.object as! NSDictionary
// takes the coordinates from the notification and converts such that the map can interpret them
let momentaryLat = (dict["latitude"] as! NSString).doubleValue
let momentaryLong = (dict["longitude"] as! NSString).doubleValue
let annotation = MKPointAnnotation()
annotation.title = dict["title"] as? String
annotation.coordinate = CLLocationCoordinate2D(latitude: momentaryLat as CLLocationDegrees, longitude: momentaryLong as CLLocationDegrees)
mapView.addAnnotation(annotation)
self.mapView.centerCoordinate = annotation.coordinate
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let identifier = "pinAnnotation"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.canShowCallout = true
}
annotationView?.annotation = annotation
return annotationView
}
}
It's the same as the Objective-C API, but uses Swift's syntax.
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(batteryLevelChanged),
name: UIDeviceBatteryLevelDidChangeNotification,
object: nil)
Or in Swift 3:
NotificationCenter.default.addObserver(
self,
selector: #selector(self.batteryLevelChanged),
name: .UIDeviceBatteryLevelDidChange,
object: nil)
If your observer does not inherit from an Objective-C object, you must prefix your method with #objc in order to use it as a selector.
#objc func batteryLevelChanged(notification: NSNotification){
//do stuff
}
Register obeserver to receive notification in your MapViewController
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.add(_:)), name: NSNotification.Name("MapViewController.coordinate.updated"), object: nil)
}
Also you can send data using userInfo property of NSNotification or send using custom object.
func add(_ notification: NSNotification) {
...
}
see pass data using nsnotificationcenter