Tải bản đầy đủ
Chapter 14. Multimedia, Contacts, Location, and Notifications

Chapter 14. Multimedia, Contacts, Location, and Notifications

Tải bản đầy đủ

The Core Location framework provides a whole suite of locationbased features for you to use—everything from a quick and effi‐
cient way to get the current location, to monitoring entry and exit
from specific regions, looking for Bluetooth beacons, to significantchange location alerts.
We’re only going to be using a tiny portion of the features of Core
Location here. If you’d like to to know more, check out Apple’s
Location and Maps Programming Guide.

There are three ways that an iOS device can figure out its location on the planet:
• Using the positioning radios, by receiving either a GPS or GLONASS signal from
orbiting satellites
• Using WiFi location, in which the iOS device uses a crowd-sourced database of
certain WiFi hotspot physical locations; depending on the hotspots that the
device can see, the device can estimate where it is in the world
• Using cell tower locations, which work essentially the same way as WiFi loca‐
tions, but with the towers that provide phone coverage
The Core Location system is designed such that you don’t need to actually know
about the details of how the device is figuring out its location. Instead, you simply ask
the iOS device to start tracking the user’s location, and it will provide it to you. It will
use whatever hardware it thinks is necessary, based on how precise a measurement
you’ve asked for.
The user’s location is private. Your app won’t have access to it
without user permission, and the user isn’t required to give it to
you. This means that any app that works with user location has to
be prepared for the user saying no.

We’ll now add the ability to attach locations to documents. Location attachments will
be JSON files that contain lat/long pairs, and they’ll be shown on a map using Map‐
Kit. The JSON files are not a specific standard or format. We’re just doing it like this
for convenience. A standard location file format doesn’t really exist, and we figure
that this is a good opportunity to work with a small amount of JSON.
MapKit provides fully featured maps, created by Apple, for you to use in your apps.
Maps can include pretty much everything the Maps app that ships with iOS and OS X
can do, from street-level map information to satellite view to 3D buildings. MapKit
also supports custom annotations, as well as automatic support for easy zooming and
panning of the map.

344

|

Chapter 14: Multimedia, Contacts, Location, and Notifications

Custom annotations can be defined by a single point (a lat/long pair) or as an overlay
that is defined by a number of points that form a shape. Annotations and overlays
behave as you’d expect, and are not just unintelligent subviews: they move and resize
appropriately when the user pans, zooms, or otherwise manipulates the map.
MapKit remains very performant, even when you have hundreds of
annotations or overlays on your map.

First, we’ll set up the application to use location services.
1. Open the application’s Info.plist file.
2. Add a new string key to the dictionary: NSLocationWhenInUseUsageDescrip
tion. Set its value to the string "We'll use your position to show where you
are on the map". This string will be shown to the user in a pop up when the app
first tries to determine location.
Don’t ever ask to access a user’s location when you don’t really
need it. Apple frowns upon this, and users will come to dis‐
trust you. Treat access to a user’s location with care.

3. Open the Assets.xcassets file.
4. Drag the Location.pdf and Current Location.pdf images into the list of images.
5. Open Document.swift.
6. Add the following code to the thumbnailImage method to return the Location
image if it’s a JSON attachment:
func thumbnailImage() -> UIImage? {
if self.conformsToType(kUTTypeImage) {
// If it's an image, return it as a UIImage
// Ensure that we can get the contents of the file
guard let attachmentContent = self.regularFileContents else {
return nil
}
// Attempt to convert the file's contents to text
return UIImage(data: attachmentContent)
}

Location Attachments

|

345

>
>
>
>
>

if self.conformsToType(kUTTypeJSON) {
// JSON files used to store locations
return UIImage(named: "Location")
}
// We don't know what type it is, so return nil
return nil
}

This additional code makes NSFileWrappers return the Location.pdf image that
you just added to the asset catalog.
Next, we’ll make the view controller that we’ll use for both adding and viewing loca‐
tions. First, we’ll set up the code, and then we’ll start building the interface.
1. Open the File menu and choose File→New.
2. Select Cocoa Touch Class and click Next.
3. Name the new class LocationAttachmentViewController and make it a subclass
of UIViewController.
4. Open LocationAttachmentViewController.swift.
5. Import the MapKit and CoreLocation frameworks at the top of the file:
import MapKit
import CoreLocation

6. Add the following line of code underneath the import statements:
let defaultCoordinate =
CLLocationCoordinate2D(latitude: -42.882743, longitude: 147.330234)

This coordinate will be used if the user’s location cannot be determined.
7. Make the LocationAttachmentViewController conform to the Attachment
Viewer and MKMapViewDelegate protocols:
class LocationAttachmentViewController: UIViewController,
AttachmentViewer, MKMapViewDelegate {

8. Add a new outlet for an MKMapView to the class, named mapView:
@IBOutlet weak var mapView : MKMapView?

9. Add the attachmentFile and document properties, which are required to con‐
form with the AttachmentViewer protocol:
var attachmentFile : NSFileWrapper?
var document : Document?

10. Open Main.storyboard and drag in a new UIViewController.

346

|

Chapter 14: Multimedia, Contacts, Location, and Notifications

11. Go to the Identity Inspector, and change the class of the view controller to Loca
tionAttachmentViewController.
12. Drag an MKMapView into the view controller’s interface.
13. Add constraints that make it fill the entire interface, with a 20-point gap at the
top of the screen for the status bar.
14. Go to the Attributes Inspector and select the Shows User Location checkbox.
15. Hold down the Control key and drag from the map view to the view controller.
Choose “delegate” from the menu that appears.
16. Hold down the Control key again and drag from the view controller to the map
view. Choose “mapView” from the menu that appears.
Now that the map view has been added, we’ll add a toolbar with a button that zooms
the map into the user’s current position, but only if it’s available. If it’s not, the button
will be dimmed out.
The location tracking system can never be relied upon to get the
user’s location. In addition to the user not granting permission—
which we’ll deal with in this section—it’s also possible for the loca‐
tion hardware to fail to get a lock on the user’s location entirely.
Apps that deal with location need to gracefully handle these kinds
of failures.

1. Drag a UIToolbar into the interface and place it at the bottom of the screen.
Make it fill the entire width of the screen, and add constraints that pin it to the
bottom and to the left and right.
2. Select the button at the left of the toolbar and go to the Attributes Inspector.
3. Set the button’s Image to Current Location.
The toolbar should now look like Figure 14-1.

Location Attachments

|

347

Figure 14-1. The updated toolbar
4. Open LocationAttachmentViewController.swift in the Assistant.
5. Connect the toolbar’s button to a new outlet in LocationAttachmentViewCon
troller called showCurrentLocationButton.
6. Connect the toolbar’s button to a new action called showCurrentLocation.
7. Add the following code to the showCurrentLocation method to make it zoom in
to the user’s location:
@IBAction func showCurrentLocation(sender: AnyObject) {
// This will zoom to the current location
self.mapView?.setUserTrackingMode(.Follow, animated: true)

348

|

Chapter 14: Multimedia, Contacts, Location, and Notifications

}

Next, we’ll add the ability to show the attachment as a pin.
1. Add the locationManager and locationPinAnnotation properties:
let locationManager = CLLocationManager()
let locationPinAnnotation = MKPointAnnotation()

2. Implement the viewDidLoad method, which requests permission to access the
location hardware:
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
}

3. Implement the didUpdateUserLocation method, which is called when Core
Location determines the user’s location and uses it to place a pin on the map:
func mapView(mapView: MKMapView,
didUpdateUserLocation userLocation: MKUserLocation) {
// If we know the user's location, we can zoom to it
self.showCurrentLocationButton?.enabled = true
// We know the user's location - add the pin!
if self.pinIsVisible == false {
let coordinate = userLocation.coordinate
locationPinAnnotation.coordinate = coordinate
self.mapView?.addAnnotation(locationPinAnnotation)
self.mapView?.selectAnnotation(locationPinAnnotation,
animated: true)
}
}

When the map view has determined the user’s location, it calls didUpdateUserLo
cation, passing in an MKUserLocation object representing the user’s location on
the planet. When this happens, we enable the current location button, allowing
the user to tap on it to zoom to their location. In addition, if we don’t already
have a location (that is, we’re creating a new location attachment), we record it
and add it to the map.
4. Implement the didFailToLocateUserWithError method, which is called if the
location system cannot locate the user:
Location Attachments

|

349

func mapView(mapView: MKMapView,
didFailToLocateUserWithError error: NSError) {
NSLog("Failed to get user location: \(error)")
// We can't show the current location
self.showCurrentLocationButton?.enabled = false
// Add the pin, but fall back to the default location
if self.pinIsVisible == false {
locationPinAnnotation.coordinate = defaultCoordinate
self.mapView?.addAnnotation(locationPinAnnotation)
}
}

If the map is unable to get the user’s location, we disable the button that shows
the current location. Additionally, we add a pin at the default coordinates.
Either way, when the user’s location has been found, or if the view controller is
displaying an existing location, or if the user’s location can’t be found, an annota‐
tion is added to the map to show the position.
An annotation is a visual marker that appears on the map. If you’ve ever seen a
marker pin in the built-in Maps application, you’ve seen an annotation in action.
Annotations are composed of two objects: the annotation itself and an annotation
view. The reason for this split is that the map can have thousands of annotations
added to it at once, but not all of them will necessarily be visible at once. It’s inef‐
ficient to keep an object that you can’t see around, especially if it’s a view, because
each view consumes quite a bit of memory.
To address this issue, the map view tries to keep as few annotation views as possi‐
ble on screen. When the user scrolls the map and brings an annotation onto the
screen, the map view calls its delegate’s viewForAnnotation method, which pre‐
pares and returns an annotation view.
5. Implement the viewForAnnotation method, which is called by the map when it
needs a view to show for an annotation:
func mapView(mapView: MKMapView,
viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
let reuseID = "Location"
if let pointAnnotation = annotation as? MKPointAnnotation {
if let existingAnnotation = self.mapView?
.dequeueReusableAnnotationViewWithIdentifier(reuseID) {
existingAnnotation.annotation = annotation
return existingAnnotation

350

|

Chapter 14: Multimedia, Contacts, Location, and Notifications

} else {
let annotationView =
MKPinAnnotationView(annotation: pointAnnotation,
reuseIdentifier: reuseID)
annotationView.draggable = true
annotationView.canShowCallout = true
return annotationView
}
} else {
return nil
}
}

Annotation views, like table view cells and collection view cells, are reused. When
an annotation view is scrolled off-screen, it’s removed from the map, but not
from memory. When you call dequeueReusableAnnotationViewWithIdenti
fier, you get back an annotation view that’s either brand new or recycled from
an earlier one.
6. Implement the pinIsVisible property, which is true if the map view contains at
least one MKPointAnnotation—that is, the user’s location pin:
var pinIsVisible : Bool {
return self.mapView!.annotations.contains({ (annotation) -> Bool in
return annotation is MKPointAnnotation
})
}

To determine when the user’s location pin has been added, we simply get the list
of annotations that exist on the map and ask if it contains any annotations that
are instances of the MKPointAnnotation class.
7. Implement viewWillAppear to prepare the map view and to add the stored loca‐
tion if it’s available:
override func viewWillAppear(animated: Bool) {
locationPinAnnotation.title = "Drag to place"
// Start by assuming that we can't show the location
self.showCurrentLocationButton?.enabled = false
if let data = attachmentFile?.regularFileContents {
do {

Location Attachments

|

351

guard let loadedData =
try NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions())
as? [String:CLLocationDegrees] else {
return
}
if let latitude = loadedData["lat"],
let longitude = loadedData["long"] {
let coordinate = CLLocationCoordinate2D(latitude: latitude,
longitude: longitude)
locationPinAnnotation.coordinate = coordinate
self.mapView?.addAnnotation(locationPinAnnotation)
}
} catch let error as NSError {
NSLog("Failed to load location: \(error)")
}
// Make the Done button save the attachment
let doneButton = UIBarButtonItem(barButtonSystemItem: .Done,
target: self, action: "addAttachmentAndClose")
self.navigationItem.rightBarButtonItem = doneButton
} else {
// Set up for editing: create a cancel button that
// dismisses the view
let cancelButton = UIBarButtonItem(barButtonSystemItem: .Cancel,
target: self, action: "closeAttachmentWithoutSaving")
self.navigationItem.leftBarButtonItem = cancelButton
// Now add the Done button that adds the attachment
let doneButton = UIBarButtonItem(barButtonSystemItem: .Done,
target: self, action: "addAttachmentAndClose")
self.navigationItem.rightBarButtonItem = doneButton
// Get notified about the user's location; we'll use
// this to add the pin when
self.mapView?.delegate = self
}
}

When the view controller’s view appears, we need to check to see if we have been
provided with a location. If we can successfully parse the data format, and extract
the latitude and longitude from it, we create an annotation and add it to the map.
352

|

Chapter 14: Multimedia, Contacts, Location, and Notifications

If we don’t have an annotation, then the reason the view controller has appeared
is to create one. As a result, we need more than just the Done button, which
closes the view controller, that the DocumentListViewController provides for us.
We need our Done button to actually create and save the annotation in the Docu
ment object, and we also need a Cancel button that closes the view controller
without creating the annotation.
To address that, we create two new UIBarButtonItems: a Done button that calls
the addAttachmentAndClose method (which we’ll add in a moment), and a Can‐
cel button that calls closeAttachmentWithoutSaving (again, we’ll add that soon).
8. Add the addAttachmentAndClose method, which adds the user’s location as an
attachment file to the Document and dismisses the view controller:
func addAttachmentAndClose() {
if self.pinIsVisible {
let location = self.locationPinAnnotation.coordinate
// Convert the location into a dictionary
let locationDict : [String:CLLocationDegrees] =
[
"lat":location.latitude,
"long":location.longitude
]
do {
let locationData = try NSJSONSerialization
.dataWithJSONObject(locationDict,
options: NSJSONWritingOptions())
let locationName : String
let newFileName = "\(arc4random()).json"
if attachmentFile != nil {
locationName
= attachmentFile!.preferredFilename ?? newFileName
try self.document?.deleteAttachment(self.attachmentFile!)
} else {
locationName = newFileName
}
try self.document?.addAttachmentWithData(locationData,
name: locationName)

Location Attachments

|

353

} catch let error as NSError {
NSLog("Failed to save location: \(error)")
}
}
self.presentingViewController?.dismissViewControllerAnimated(true,
completion: nil)
}

This code creates a dictionary that stores the current location of the pin and con‐
verts it to JSON. It then gives it to the Document, which adds it as an attachment.
Finally, it dismisses the view controller.
9. Add the closeAttachmentWithoutSaving method, which dismisses the view
controller without making changes to the Document:
func closeAttachmentWithoutSaving() {
self.presentingViewController?.dismissViewControllerAnimated(true,
completion: nil)
}

The simpler cousin of addAttachmentAndClose, closeAttachmentWithoutSav
ing simply closes the view controller.
Next, we’ll connect the document view controller to the location attachment view
controller with a popover segue.
1. Open Main.storyboard.
2. Hold down the Control key, and drag from the document view controller to the
location attachment view controller. Choose “popover” from the list that appears.
3. Select the new segue and drag from the well next to Anchor to the document
view controller’s view.
4. Give the segue an identifier by opening the Attributes Inspector and setting the
segue’s identifier to ShowLocationAttachment.
Next, we’ll add the ability to add new location attachments. First, we’ll add an entry to
the list of attachment types in addAttachment, and then we’ll add a method that
shows the LocationAttachmentViewController if the user chooses to add a location
attachment.
1. Add the following code to addAttachment:
func addAttachment(sourceView : UIView) {
let actionSheet
= UIAlertController(title: "Add attachment",
message: nil,
preferredStyle: UIAlertControllerStyle
.ActionSheet)

354

|

Chapter 14: Multimedia, Contacts, Location, and Notifications