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