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.
AppDelegate
ViewController
creates the
MKMapView
(implicitly),
the
CLLocationManager
,
and the
CLGeocoder
.
The view controller also acts as the delegate of the
CLLocationManager
.
Pin
adopts the
MKAnnotation
protocol.
Map-Info.plist
.
Contains the properties
NSLocationWhenInUseUsageDescription
and
NSLocationAlwaysUsageDescription
,
plus two extra items in the property
Required
Device Capabilities.
Pace.gpx
.
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)
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
.
Make the app believe we’re at the latitude and longitude of
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
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.
update Lat 40.7101843° Long -74.0061474° update Lat 40.7101843° Long -74.0061474° placemark.subThoroughfare = 161 placemark.thoroughfare = William St
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?’
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.
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"); }
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.
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;
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 |