MKMapView,
CLLocationManager and its delegate,
CLGeocoder,
MKAnnotation and its Annotation View,
Overlay and its Overlay View

In the JavaScript project, the view controller created a UIWebView that displayed a Google map. In the current project, the view controller creates a MKMapView, which is a subclass of UIView specialized for displaying one Google map. The map fills the MKMapView, so it doesn’t need a backgroundColor.

The view controller also creates a CLLocationManager, which finds your current location using a lot of electricity. Our view controller acts as the delegate of the CLLocationManager. When the CLLocationManager figures out where we are, the locationManager(_:didUpdateLocations:) method of the delegate is called. This method receives a CLLocation object containing the latitude, longitude, altitude, accuracy, etc. (Check out the other interesting methods of the delegate.) See Getting the User’s Location.

The locationManager(_:didUpdateLocations:) method turns off the CLLocationManager to save electricity. It then tells the map to display a 100-meter-wide region containing the current location. With our aspect ratio of approximately 9:16 (375 × 667 pairs of pixels on an iPhone 6), the region should be 100 × 16 ÷ 9 = 177.86 meters from north to south.

locationManager(_:didUpdateLocations:) also asks the CLGeocoder to look up the street address of the location. See Getting Placemark Information Using CLGeocoder. When the address is found, the CLGeocoder executes the ^{block} of code (i.e., closure) passed as the second argument of reverseGeocodeLocation(_:completionHandler:). We saw blocks in Hello exercise 9 and in Animate.

This block creates an MKAnnotation and puts it into the map. An MKAnnotation can be anything that has a title, subtitle, and latitude and longitude. For example, the Pin object I created is the simplest kind of MKAnnotation. Note that the pin looks like an Apple pin, not a Google Maps pin. See Annotating Maps.

Source code in Map.zip

  1. Class AppDelegate
  2. Class ViewController creates the MKMapView (implicitly), the CLLocationManager, and the CLGeocoder. The view controller also acts as the delegate of the CLLocationManager.
  3. Class Pin adopts the MKAnnotation protocol.
  4. Property list Map-Info.plist. Contains the properties NSLocationWhenInUseUsageDescription and NSLocationAlwaysUsageDescription, plus two extra items in the property Required Device Capabilities.
  5. GPX file Pace.gpx.

Create the project

Tell the view controller that the view it creates should be of class MKMapView. (MK stands for Map Kit.) Select the Main.storyboard file in the Xcode Project Navigator. In the left pane of the center panel, open the View Controller Scene as far as the View in the following list, and select the View.

▼View Controller Scene
   ▼View Controller
      Top Layout Guide
      Bottom Layout Guide
      View

In the right panel of Xcode, select the icon for the Identity Inspector. It’s a rectangle containing a smaller rectangle in its upper left corner.
Custom Class
Class: MKMapView
Module: None (leave it blank)

Properties

Add the NSLocationWhenInUseUsageDescription and NSLocationAlwaysUsageDescription properties to the Info.plist file. We can’t add them in the normal way, so use the following workaround. In the Supporting Files folder of the Xcode Project Navigator, control-click on Info.plist and select
Open As → Source Code.
The file ends with these two lines:

</dict>
</plist>

Insert the following four lines immediately above the two lines shown above:

	<key>NSLocationWhenInUseUsageDescription</key>
	<string>This app needs Core Location for demonstration purposes.</string>
	<key>NSLocationAlwaysUsageDescription</key>
	<string>This app needs Core Location for demonstration purposes.</string>

In the Supporting Files folder of the Xcode Project Navigator, control-click on Info.plist and select
Open As → property List.
Admire your two new properties.

The Info.plist file already contains a propery named “Required Device Capabilities”. Click its open triangle ▶ to see that this property aleady contains a list of one item, armv7. If you want to allow this app to run only on devices that have location services, add the location-services item to this list. If you want to allow this app to run only on devices that have location services provided by a GPS, you must also add the gps item to this list. See Requiring the Presence of Location Services.

The view controller imports the CoreLocation and MapKit frameworks. Class Pin is a subclass of class NSObject. Pin.h imports the header file MapKit.h.

Latitude and longitude

Make the app believe we’re at the latitude and longitude of

163 William Street
New York, NY 10038


Find the latitude and longitude of this address by pointing your browser at the following URL. See Geocoding Requests.

https://maps.googleapis.com/maps/api/geocode/json?address=163+William+Street,+New+York,+NY+10038+USA

The page of JSON response will contain the following. Positive latititude is north of the equator; negative longitude is west of the prime meridian. Angles are in degrees, not radians.

"location" : {
               "lat" : 40.7101843,
               "lng" : -74.0061474
            }

Select the Supporting Files folder in the Xcode Project Navigator.
File → New → File…
Choose a template for your new file:
iOS Resource     GPX File
Next
Save As: Pace.gpx
Create
Edit your new file Pace.gpx as follows:

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">

    <wpt lat="40.7101843" lon="-74.0061474">
         <name>Pace University, 163 William Street</name>
	</wpt>

</gpx>

In Xcode,
Product → Scheme → Edit Scheme…
Select Run in the left pane, Options at the top.
Core Location ☑ Allow Location Simulation
Default Location: Pace
Close

Run the project

We don’t have to do anything with the System Preferences app on the Macintosh. The iOS Simulator is not in the list of apps that request the Mac’s location. See
System Preferences → Security & Privacy → Privacy → Location Services
☑ Enable Location Services

But we do have to go into the Settings app in the iOS Simulator.
Settings → Privacy → Location
Turn on Location Services.

Then run the app.
Allow “Map” to access your location while you use the app?
This app needs Core Location for demonstration purposes.
Don’t Allow/Allow
Press Allow.
If the app still doesn’t work,
iOS Simulator → Reset Content and Settings…
Reset
The quit the simulator and launch it again.

Tap the red pin to display its title and subtitle. Double tap or spread to zoom in, pinch to zoom out. To pinch the iOS Simulator, hold down the option key while dragging.

Output from print

update Lat 40.7101843° Long -74.0061474°
update Lat 40.7101843° Long -74.0061474°
placemark.subThoroughfare = 161
placemark.thoroughfare = William St

Things to try

  1. Display the red pin even if the geocoder fails. If it fails, the pin should display only the latitude and longitude, not the street address.

  2. The MKMapView becomes visible and displays a world map before it is given the location to display. Keep the MKMapView hidden until you give a value to the region property of the MKMapView. Even if the MKMapView is hidden, it will still display the action sheet ‘Allow “Map” to access your location?’

  3. Even when the region property has been given a value, it may still take some time until the tiles of the map have finished downloading from Google into the MKMapView. I wish I could ask you to keep the MKMapView hidden until the mapViewDidFinishLoadingMap(_:) method of the MKMapView’s delegate is called. (Let the view controller be the MKMapView’s delegate. The view controller will now be the delegate for two different objects.) But when I run the app on the simulator, mapViewDidFinishLoadingMap(_:) is called only the first time the map is downloaded loaded from Google. Even if I erase the app from the simulator, and quit the simulator, and quit Xcode itself, and then re-launch Xcode, mapViewDidFinishLoadingMap(_:) is not called when I run the app again. The tiles of the map appear anyway, which means they are being cached somewhere.

    In other words, the purpose (or at least the usefulness) of mapViewWillStartLoadingMap(_:) and mapViewDidFinishLoadingMap(_:) is not to tell us when all the tiles have been downloaded. (The tiles have probably already been downloaded.) Their purpose is to delimit the interval of time during which we have to stay connected to the Internet.

    To verify this theory, let the view controller be the delegate of the MKMapView object. The view controller will have to adopt the MKMapViewDelegate protocol, and the delegate property of the MKMapView will have to refer to the view controller. Then run the app again, but with a location that is one degree of latitude farther north. In locationManager(_:didUpdateLocations:), change the first argument of MKCoordinateRegionMakeWithDistance from location.coordinate to

    	//a new location, one degree (about 60 miles) north of the old location
    
    	CLLocationCoordinate2DMake(
    		location.coordinate.latitude + 1,
    		location.coordinate.longitude)
    
    Run the app again and you’ll see that mapViewDidFinishLoadingMap(_:) is called. Run the app yet again and mapViewDidFinishLoadingMap(_:) is not called.


  4. Now that the MKMapView has a delegate, we no longer have to use the default picture (a red pushpin) for the annotation. Get an image (Apple prefers png, but jpg will do) with a transparent background and add it to the project. For example, get the image Bronx_unselected@2x.png from TabBar, and save it on your Mac Desktop as bronx.png. Then add bronx.png to the Images.xcassets file of the project.

    Add the following method to the MKMapViewDelegate (which is the view controller). For simplicity, it always returns an annotation view with exactly the same picture.

    	//For simplicity, we ignore the annotation argument.
    	//That means that every annotation will look like bronx.png.
    
    	func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
    		let v: MKAnnotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: nil);
    		v.image = UIImage(named: "bronx.png")!;
    		return v;
    	}
    
    Even better, reuse an existing MKAnnotationView instead of creating a new one with every call to mapView(_:viewForAnnotation:).

    Apple’s default image is a 64 × 78 pixel pin with a transparent background. I stole it by creating a new project and putting the following code in the viewDidLoad method of the view controller.

    		//import MapKit for MKPinAnnotationView, and change myname to your name.
    
    		let pinAnnotationView: MKPinAnnotationView = MKPinAnnotationView();
    		pinAnnotationView.pinColor = MKPinAnnotationColor.Green;
    		pinAnnotationView.pinColor = MKPinAnnotationColor.Red;	//or Purple
    
    		//Can get .image only if assigned pin color at least once.
    		let data: NSData = UIImagePNGRepresentation(pinAnnotationView.image);
    
    		if data.writeToFile("/Users/myname/Desktop/pin.png", atomically: true) {
    			print("created /Users/myname/Desktop/pin.png");
    		} else {
    			print("writeToFile failed");
    		}
    



  5. The CLLocation received by the location manager delegate contains more than just a latitude and longitude. It also contains a (horizontal) accuracy in meters. Create a subclass of MKAnnotationView with a drawRect(_:) method that will draw a circle whose radius is the accuracy. See the Overview at the top of this page. Tell the mapView(_:viewForAnnotation:) method to create an object of this new class instead of instantiating a plain vanilla MKAnnotationView. Or simply see class MKCircle below.

  6. The above annotation and its annotation view marked a point on the map. An overlay and its overlay view mark a region on the map. See Displaying Overlays on a Map. A simple example of an overlay and its overlay view is an MKPolygon and its MKPolygonView. Let’s make one that is a purple rectangle covering the NYU campus.

    Add the following property to the MKMapViewDelegate (which is the view controller).

    	var polygon: MKPolygon = MKPolygon();
    

    Initialize the polygon (again) and add it to the mapView in the viewDidLoad() method of the view controller. (To get the latitudes and longitudes from Google Maps, use the plotter in Manhattan.)

    		//Latitude and longitude of the four corners of NYU campus:
    		var coordinates: [CLLocationCoordinate2D] = [
    			CLLocationCoordinate2DMake(40.728804, -73.99500),  //West 4th St. & Mercer St.
    			CLLocationCoordinate2DMake(40.730978, -73.999625), //West 4th St. & MacDougal St.
    			CLLocationCoordinate2DMake(40.730438, -74.000092), //West 3rd St. & MacDougal St.
    			CLLocationCoordinate2DMake(40.728202, -73.99555)   //West 3rd St. & Mercer St.
    		];
    
    		polygon = MKPolygon(coordinates: &coordinates, count: coordinates.count);
    		mapView.addOverlay(polygon);
    

    Add the following method to the MKMapViewDelegate (which is the view controller).

    	func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
    		let polygonRenderer: MKPolygonRenderer = MKPolygonRenderer(overlay: overlay);
    		polygonRenderer.fillColor = UIColor.purpleColor();
    		return polygonRenderer;
    	}
    

    Build and run. Swipe over to the Washington Square campus of NYU and see if it’s covered by a purple rectangle. Then make the rectangle semi-transparent (alpha = ½) with opaque edges (alpha = 1). Use the official shade of NYU purple:

    		polygonRenderer.fillColor = UIColor(
    			red: 82 / 250.0,
    			green: 6 / 255.0,
    			blue: 145 / 255.0,
    			alpha: 0.5);
    
    		polygonRenderer.strokeColor = UIColor(
    			red: 82 / 250.0,
    			green: 46 / 255.0,
    			blue: 145 / 255.0,
    			alpha: 1.0);
    
    		polygonRenderer.lineWidth = 3;
    
  7. You can do more than just specify the fillColor and outline of the overlay view. See the other properties that class MKPolygonRenderer inherits from class MKOverlayPathRenderer.

    There are other types of overlays and overlay views. See the Map Kit Framework.

    Class that adopts the
    MKOverlay Protocol
    Class derived from class
    MKOverlayRenderer
    MKPolygon MKPolygonRenderer
    MKCircle MKCircleRenderer
    MKPolyline MKPolylinerenderer