In this chapter, we will have a look at some awesome updates to the MapKit
and CoreLocation
frameworks.
Use the requestLocation()
method of the CLLocationManager
class. The new location will be sent to your location manager’s locationManager(_:didUpdateLocations:)
delegate method. Errors will be reported on locationManager(_:didFailWithError:)
. You can make only one request to this method at any given time. A new request will cancel the previous one.
Place a button on your interface inside IB and then hook it up to a method in your code called requestLocation()
. Then go into your Info.plist file and set the value of the NSLocationWhenInUseUsageDescription
key to a valid string that explains to the user why you want to get her location. You will also have to import the CoreLocation
framework and make your view controller conform to CLLocationManagerDelegate
.
Implement a variable in your view controller to represent the location manager:
lazy
var
locationManager
:
CLLocationManager
=
{
let
m
=
CLLocationManager
()
m
.
delegate
=
self
m
.
desiredAccuracy
=
kCLLocationAccuracyNearestTenMeters
return
m
}()
When your button is pressed, request access to the user’s location. This requests users location to be delivered to your app only when it is the foreground app. As soon as your app is sent to the background, iOS stops delivering location updates to you:
@
IBAction
func
requestLocation
()
{
locationManager
.
requestWhenInUseAuthorization
()
}
Then wait for the user to accept or reject the request. If everything is going smoothly, request the user’s location:
func
locationManager
(
manager
:
CLLocationManager
,
didChangeAuthorizationStatus
status
:
CLAuthorizationStatus
)
{
if
case
.
AuthorizedWhenInUse
=
status
{
manager
.
requestLocation
()
}
else
{
//TODO: we didn't get access, handle this
}
}
Last but not least, wait for the location gathering mechanism to fail or succeed:
func
locationManager
(
manager
:
CLLocationManager
,
didUpdateLocations
locations
:
[
CLLocation
])
{
//TODO: now you have access to the location. do your work
}
func
locationManager
(
manager
:
CLLocationManager
,
didFailWithError
error
:
NSError
)
{
//TODO: handle the error
}
When linked against iOS 9, apps that want to ask for a user’s location when the app is in the background have to set the allowsBackgroundLocationUpdates
property of their location manager to true
. We are going to have to have a look at an example. Start a single view controller app, place a button on your UI with IB, and give it a title similar to “Request background location updates”. Then hook it to a method in your view controller and name the method requestBackgroundLocationUpdates()
. In your Info.plist
file, set the string value of the NSLocationAlwaysUsageDescription
key and make sure that it explains exactly why you want to access the user’s location even in the background. Then go into the Capabilities section of your target, and under Background Modes, enable “Location updates” (see Figure 8-1).
Now import CoreLocation
in your code and make your view controller conformant to CLLocationManagerDelegate
. Create your location manager and make sure that the allowsBackgroundLocationUpdates
property is set to true
.
lazy
var
locationManager
:
CLLocationManager
=
{
let
m
=
CLLocationManager
()
m
.
delegate
=
self
m
.
desiredAccuracy
=
kCLLocationAccuracyNearestTenMeters
m
.
allowsBackgroundLocationUpdates
=
true
return
m
}()
When the user presses the button, ask for location updates:
@
IBAction
func
requestBackgroundLocationUpdates
()
{
locationManager
.
requestAlwaysAuthorization
()
}
Wait until the user accepts the request and then start looking for location updates:
func
locationManager
(
manager
:
CLLocationManager
,
didChangeAuthorizationStatus
status
:
CLAuthorizationStatus
)
{
if
case
CLAuthorizationStatus
.
AuthorizedAlways
=
status
{
manager
.
startUpdatingLocation
()
}
}
Last but not least, implement the usual location manager methods to get to know when the user’s location has changed:
func
locationManager
(
manager
:
CLLocationManager
,
didUpdateLocations
locations
:
[
CLLocation
])
{
//TODO: now you have access to the location. do your work
}
func
locationManager
(
manager
:
CLLocationManager
,
didFailWithError
error
:
NSError
)
{
//TODO: handle the error
}
Let’s check out an example. Create a single view controller project and dump a map view on top of your view. Make sure that you set the delegate of this map view to your view controller. Also link it to a variable named map
in your view controller.
In the view controller, we are going to create annotations with reusable identifiers, so let’s use the color as the ID:
import
MapKit
extension
UIColor
{
final
func
toString
()
->
String
{
var
red
=
0.0
as
CGFloat
var
green
=
0.0
as
CGFloat
var
blue
=
0.0
as
CGFloat
var
alpha
=
0.0
as
CGFloat
getRed
(
&
red
,
green
:
&
green
,
blue
:
&
blue
,
alpha
:
&
alpha
)
return
"(Int(red))(Int(green))(Int(blue))(Int(alpha))"
}
}
Now we create our annotation:
class
Annotation
:
NSObject
,
MKAnnotation
{
var
coordinate
:
CLLocationCoordinate2D
var
title
:
String
?
var
subtitle
:
String
?
init
(
coordinate
:
CLLocationCoordinate2D
,
title
:
String
,
subtitle
:
String
){
self
.
coordinate
=
coordinate
self
.
title
=
title
self
.
subtitle
=
subtitle
}
}
Now ensure that your view controller conforms to the MKMapViewDelegate
protocol, define the location that you want to display on the map, and create an annotation for it:
let
color
=
UIColor
(
red
:
0.4
,
green
:
0.8
,
blue
:
0.6
,
alpha
:
1.0
)
let
location
=
CLLocationCoordinate2D
(
latitude
:
59.33
,
longitude
:
18.056
)
lazy
var
annotations
:
[
MKAnnotation
]
=
{
return
[
Annotation
(
coordinate
:
self
.
location
,
title
:
"Stockholm Central Station"
,
subtitle
:
"Stockholm, Sweden"
)]
}()
When your view appears on the screen, add the annotation to the map:
override
func
viewDidAppear
(
animated
:
Bool
)
{
super
.
viewDidAppear
(
animated
)
map
.
removeAnnotations
(
annotations
)
map
.
addAnnotations
(
annotations
)
}
And when the map view asks for an annotation view for your annotation, return an annotation view with the custom color (see Figure 8-2):
func
mapView
(
mapView
:
MKMapView
,
viewForAnnotation
annotation
:
MKAnnotation
)
->
MKAnnotationView
?
{
let
view
:
MKPinAnnotationView
if
let
v
=
mapView
.
dequeueReusableAnnotationViewWithIdentifier
(
color
.
toString
())
where
v
is
MKPinAnnotationView
{
view
=
v
as
!
MKPinAnnotationView
}
else
{
view
=
MKPinAnnotationView
(
annotation
:
annotation
,
reuseIdentifier
:
color
.
toString
())
}
view
.
pinTintColor
=
color
return
view
}
Recipe 8.4 and Recipe 8.5
Create your project just as you did in Recipe 8.3. In this recipe, I am going to reuse a lot of code from the aforementioned recipe, except for the implementation of the mapView(_:viewForAnnotation:)
delegate method of our view controller. Instead, we are going to construct instances here of MKAnnotationView
and then set the detail callout accessory view:
func
mapView
(
mapView
:
MKMapView
,
viewForAnnotation
annotation
:
MKAnnotation
)
->
MKAnnotationView
?
{
let
view
:
MKAnnotationView
if
let
v
=
mapView
.
dequeueReusableAnnotationViewWithIdentifier
(
identifier
){
//reuse
view
=
v
}
else
{
//create a new one
view
=
MKAnnotationView
(
annotation
:
annotation
,
reuseIdentifier
:
identifier
)
view
.
canShowCallout
=
true
if
let
img
=
UIImage
(
named
:
"Icon"
){
view
.
detailCalloutAccessoryView
=
UIImageView
(
image
:
img
)
}
if
let
extIcon
=
UIImage
(
named
:
"ExtIcon"
){
view
.
image
=
extIcon
}
}
return
view
}
Figure 8-3 shows the image of an annotation on a map. The image inside the callout is the detail callout accessory view.
I am using two public domain images in this recipe. You also can find public domain images on Google.
Recipe 8.3 and Recipe 8.5
Place a map view on your view and set the appropriate constraints on it so that it stretches across the width and height of your view controller’s view. This is really optional, but useful so the user can see the map view properly on all devices. Then follow what I talked about in Recipe 8.3 to place an annotation on the map. Write a code similar to the following in a method such as viewDidLoad
:
map
.
showsCompass
=
true
map
.
showsTraffic
=
true
map
.
showsScale
=
true
The results will be similar to those shown in Figure 8-4. The scale is shown on top left and the compass on the top right. You have to rotate the map for the compass to appear.
Recipe 8.3 and Recipe 8.4
Mark your app as a routing app and construct an instance of the MKDirectionsRequest
class. Set the transportType
property of that request to Transit
and send your request to Apple to calculate an estimated time of arrival (ETA), using the calculateETAWithCompletionHandler(_:)
method of the MKDirections
class.
We use Geo JSON files in this recipe, so read the spec for that format first, please.
Create a single-view application. Then head to the Capabilities tab in Xcode, enable the Maps section, and mark the routing options that you believe your app will be able to provide (see Figure 8-5). I’ve enabled all these items for demonstration purposes. You probably wouldn’t want to enable all of these in your app.
Create a new Directions.geoJson file in your app and then head over to GeoJson.io to create the polygon that defines your routing coverage area. Then copy and paste the generated content and place it in the aforementioned file in your project. Now go and edit your target’s scheme. Under Run and then Options, find the Routing App Coverage file section and select your file (see Figure 8-6).
You can always go to GeoJsonLint to validate your Geo JSON files.
This will allow the maps app to open my app whenever the user asks for transit information on the iOS maps app. Now code the application(_:openURL:options:)
method of your app delegate and handle the routing request there:
func
application
(
app
:
UIApplication
,
openURL
url
:
NSURL
,
options
:
[
String
:
AnyObject
])
->
Bool
{
guard
MKDirectionsRequest
.
isDirectionsRequestURL
(
url
)
else
{
return
false
}
//now we have the url
let
req
=
MKDirectionsRequest
(
contentsOfURL
:
url
)
guard
req
.
source
!=
nil
&&
req
.
destination
!=
nil
else
{
return
false
}
req
.
transportType
=
.
Transit
req
.
requestsAlternateRoutes
=
true
let
dir
=
MKDirections
(
request
:
req
)
dir
.
calculateETAWithCompletionHandler
{
response
,
error
in
guard
let
resp
=
response
where
error
==
nil
else
{
//handle the error
(
error
!
)
return
}
(
"ETA response = (resp)"
)
}
return
true
}
Now open the maps app and ask for directions from one location to another. If the maps app couldn’t handle the request, it will show a little “View Routing Apps” button. Regardless of whether the maps app could or couldn’t show the routing options, the user can always press the little navigation button to open alternative routing apps (see Figure 8-7). Your app will be displayed in the list of routing apps if the user asks for a routing option you support, and if the starting and stopping points are within the shape you defined in your Geo JSON file. When the user opens your app, your app delegate will be informed and will calculate an ETA.
Let’s create a single-view controller app and place a button on the view controller to open a map. Set the title of this button to something like “Open maps app in transit mode.” Then hook it up to your view controller. For every coordinate of type CLLocationCoordinate2D
, you have to create an instance of MKPlacemark
and then from the placemark, create an instance of MKMapItem
.
Here is the source map item:
let
srcLoc
=
CLLocationCoordinate2D
(
latitude
:
59.328564
,
longitude
:
18.061448
)
let
srcPlc
=
MKPlacemark
(
coordinate
:
srcLoc
,
addressDictionary
:
nil
)
let
src
=
MKMapItem
(
placemark
:
srcPlc
)
Followed by the destination map item:
let
desLoc
=
CLLocationCoordinate2D
(
latitude
:
59.746148
,
longitude
:
18.683281
)
let
desPlc
=
MKPlacemark
(
coordinate
:
desLoc
,
addressDictionary
:
nil
)
let
des
=
MKMapItem
(
placemark
:
desPlc
)
You can use the Get Latitude Longitude website to find the latitude and longitude of any point on the map.
Now we can launch the app, under transit mode, with the source and the destination points:
let
options
=
[
MKLaunchOptionsDirectionsModeKey
:
MKLaunchOptionsDirectionsModeTransit
]
MKMapItem
.
openMapsWithItems
([
src
,
des
],
launchOptions
:
options
)
Recipe 8.5 and Recipe 8.6
Set the mapType
property of your MKMapView
to either HybridFlyover
or SatelliteFlyover
.
The flyover mode of a map view represents the map as if it were on a globe, rather than flat. So keep that in mind when placing a camera on the map to show to the user.
Let’s start off with a single-view controller app. Place a map view on your view and hook it up to your code. I’ve named mine “map.” When your view gets loaded, make sure that your map type is one of the aforementioned flyover modes:
map
.
mapType
=
.
SatelliteFlyover
map
.
showsBuildings
=
true
Then when your view appears on the screen, set the camera on your map:
let
loc
=
CLLocationCoordinate2D
(
latitude
:
59.328564
,
longitude
:
18.061448
)
let
altitude
:
CLLocationDistance
=
500
let
pitch
=
CGFloat
(
45
)
let
heading
:
CLLocationDirection
=
90
let
c
=
MKMapCamera
(
lookingAtCenterCoordinate
:
loc
,
fromDistance
:
altitude
,
pitch
:
pitch
,
heading
:
heading
)
map
.
setCamera
(
c
,
animated
:
true
)
Run this code on a real device (this doesn’t work very well on simulator) and you’ll get a display along the lines of Figure 8-8.
Recipe 8.5, Recipe 8.6, and Recipe 8.7