I has been a while since I don't do iOS. These are my notes, hopefully helpful for others.
Just show me code
Here you go. Full working example :)
Show a map
Very simple, create a view controller with a MapKit MapView object and you are done. A MapView without delegate is still usable however I set it (in interface builder) for future use.
import UIKit
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate {
    @IBOutlet fileprivate weak var mapView: MKMapView!
}
Get and show current location
Get current location is a task of CoreLocation. We need to receive updates from 
CoreLocationManagerDelegate so render current location in our mapView.
Location itself will be stored in 
fromLocation and 
fromLocationAnnotation will be used to represent a pin at such location.
import UIKit
import MapKit
import CoreLocation
class MapInfoDetailViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
    @IBOutlet fileprivate weak var mapView: MKMapView!
    fileprivate var locationManager: CLLocationManager?
    fileprivate var fromLocation: CLLocation?
    fileprivate var fromLocationAnnotation: MKPointAnnotation?
}
We start the location manager
override func viewDidLoad() {
    super.viewDidLoad()
    // Start location manager
    if CLLocationManager.locationServicesEnabled() {
        locationManager = CLLocationManager()
        locationManager?.delegate = self
        locationManager?.desiredAccuracy = kCLLocationAccuracyBest
        locationManager?.requestAlwaysAuthorization()
        locationManager?.startUpdatingLocation()
    }
}
Note that in recent versions of iOS we need to add the following to the Info.plist otherwise CoreLocation will not work
<key>NSLocationAlwaysUsageDescription</key>
<string>🍌Short explanation of why you will require location services here🍌</string>
Here is the method that receives locations updates. This method is called several times in very short intervals
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.last else { return }
    fromLocation = location
    if fromLocationAnnotation == nil {
        // First time: Create and add annotation
        fromLocationAnnotation = MKPointAnnotation()
        fromLocationAnnotation!.coordinate = fromLocation!.coordinate
        fromLocationAnnotation!.title = "Current Location"
        mapView.addAnnotation(fromLocationAnnotation!)
    } else {
        // Not first time: Update annotation
        mapView.removeAnnotation(fromLocationAnnotation!)
        fromLocationAnnotation!.coordinate = fromLocation!.coordinate
        mapView.addAnnotation(fromLocationAnnotation!)
    }
    mapView.showAnnotations(mapView.annotations, animated: true)
    if location.horizontalAccuracy < 30 {
        // IMO 30m is accurate enough to turn off location services to save battery :)
        locationManager?.stopUpdatingLocation()
    }
}
Previous method adds an MKPointAnnotation to the mapView. MKMapView will render the default annotation view for the given annotation. So far we our current location :)
Customize the pin
To customize the view we need to implement MKMapViewDelegate method. 
When we add 
MKAnnotation (usually a 
MKPointAnnotation) the map view will consult its delegate to see if there is a view for the given annotation. If we return nil or do not implement the delegate method is will draw the default view (a red pin).
Usually you want to create a view with some extra information special to the location (For example number of likes for that place, an explanation of the place, etc) and a way to pass data from your model to the view is via a subclass of MKAnnotation. Each MKAnnotationView object will have a reference to an MKAnnotation so we have put data here.
import MapKit
class PlaceAnnotation: MKPointAnnotation {
    var label: String?
}
To use it instead of creating 
MKPointAnnotation we use our own:
...
// Instead of:
// fromLocationAnnotation = MKPointAnnotation()
// We can create our MKAnnotation and pass any data we might need later
let annotation = PlaceAnnotation()
annotation.label = "出発"
fromLocationAnnotation = annotation
...
The delegate method:
public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    guard let annotation = annotation as? PlaceAnnotation else {
        // Other annotations will show the default pin
        return nil
    }
    // Place annotation
    var annotationView: MKAnnotationView?
    let reuseId = String(describing: PlaceAnnotation.self)
    annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
    if annotationView == nil {
        // Create annotation view
        annotationView = MKAnnotationView(
            annotation: annotation,
            reuseIdentifier: reuseId)
        annotationView?.canShowCallout = true
        annotationView?.image = image(text: annotation.label)
    } else {
        // Update annotation view (update the least possible)
        annotationView?.annotation = annotation
        annotationView?.image = image(text: annotation.label)
    }
    return annotationView
}
I am using a regular MKAnnotationView and simply customizing the image according the given data (I am creating an UIImage on the fly). In my small app I have just a few annotations so this does not cost me anything. If performance becomes a problem then we should create subclass 
MKAnnotationView and render things there rather than setting a different 
UIImage every time, to really reuse it.
If you are curious about 
image(text: annotation.label) check code 
here.
Get and show location from an address
The act of find coordinates from a address text is called Geo Coding. Happily there is a geocoder class in 
CoreLocation so lets use it:
fileprivate var toLocation: CLLocation?
...
func getLocationAndShowRoute(address: String) {
    let geocoder = CLGeocoder()
    geocoder.geocodeAddressString(address) { (placemarks, error) in
        if let error = error {
            print("Geocoder error. " + error.localizedDescription)
            return
        }
        guard let placemark = placemarks?.last else {
            print("Geocoder error. No placemarks found")
            return
        }
        guard let location = placemark.location else {
            print("Geocoder error. No location in placemark")
            return
        }
        // Add location
        let annotation = MKPointAnnotation()
        annotation.coordinate = location.coordinate
        annotation.title = address
        self.mapView.addAnnotation(annotation)
        self.toLocation = location
    }
}
Draw route between two locations
By now we should have two locations:
- fromLocation: current location found with help of- CLLocationManager
- toLocation: an arbitrary location found by geocoding an address
func showRoute(transportType: MKDirectionsTransportType) {
    guard let from = fromLocation else {
        print("showRoute: no fromLocation")
        return
    }
    guard let to = toLocation else {
        print("showRoute: no toLocation")
        return
    }
    
    // Search routes in MapKit (Japanese article)
    // http://qiita.com/oggata/items/18ce281d5818269c7281
    let fromPlacemark = MKPlacemark(coordinate: from.coordinate, addressDictionary: nil)
    let toPlacemark = MKPlacemark(coordinate: to.coordinate, addressDictionary: nil)
    
    let fromItem = MKMapItem(placemark:fromPlacemark)
    let toItem = MKMapItem(placemark:toPlacemark)
    
    let request = MKDirectionsRequest()
    request.source = fromItem
    request.destination = toItem
    request.requestsAlternateRoutes = false // only one route
    request.transportType = transportType
    
    let directions = MKDirections(request:request)
    directions.calculate { response, error in
        if let error = error {
            print("Route search error. " + error.localizedDescription)
            return
        }
        guard let route = response?.routes.last else {
            print("Route search error. No routes found")
            return
        }
        self.mapView.removeOverlays(self.mapView.overlays)
        self.mapView.add(route.polyline)
    }
}
Tadaa!
Full code available 
here.
0 comments :
Post a Comment