Potloc/PotlocViewController.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Contains the PotlocViewController which shows how to use CLLocationManager and WCSession together to communicate between an iPhone and an Apple Watch. |
*/ |
import UIKit |
import WatchConnectivity |
import CoreLocation |
/** |
The `PotlocViewController` is responsible for maintaining the `CLLocationManager` |
and the `WCSession`. When the watch sends a start or stop message, the `PotlocViewController` |
informs the `CLLocationManager` to act accordingly. As the location manager accumulates |
locations, the `PotlocViewController` maintains a cumulative count of how many |
locations have been received. On a five second timeout, the `PotlocViewController` sends |
the location count to the watch to be processed as needed. |
The `PotlocViewController` can also start and stop updating location using the |
`startStopUpdatingLocationButton`. When location updates are started or stopped, |
both views are informed of the change so they can both update accordingly. |
When sending streamed information such as location updates to the watch, it |
is recommended to batch the updates and send them less frequently than received. |
*/ |
class PotlocViewController: UIViewController, WCSessionDelegate, CLLocationManagerDelegate { |
// MARK: Properties |
/// Default WatchConnectivity session for communicating with the watch. |
let session = WCSession.default() |
/// Location manager used to start and stop updating location. |
let manager = CLLocationManager() |
/// Indicates whether the location manager is updating location. |
var isUpdatingLocation = false |
/// Cumulative count of received locations. |
//var receivedLocationCount: AnyObject = 0 |
var receivedLocationCount = 0 |
/// The number of locations that will be sent in a batch to the watch. |
var locationBatchCount = 0 |
/** |
Timer to send the cumulative count to the watch. |
To avoid polluting IDS traffic, its better to send batch updates to the watch |
instead of sending the updates as they arrive. |
*/ |
var sessionMessageTimer = Timer() |
/** |
Label to show the status of the Location Manager: running or not running. |
This label is useful for debugging. |
*/ |
@IBOutlet weak var managerStatusLabel: UILabel! |
/// Static text informing the user of the meaning of the location batch size label. |
@IBOutlet weak var locationBatchSizeTitleLabel: UILabel! |
/// Indicates to the user the number of locations that will be sent to the watch. |
@IBOutlet weak var locationBatchSizeLabel: UILabel! |
/// Static text informing the user of the meaning of the received location count label. |
@IBOutlet weak var receivedLocationCountTitleLabel: UILabel! |
/// Indicates to the user the total number of locations that have been received since starting the app. |
@IBOutlet weak var receivedLocationCountLabel: UILabel! |
/// Button to start or stop updating location. Outlet is used to set the text depending on the action. |
@IBOutlet weak var startStopUpdatingLocationButton: UIButton! |
// MARK: Localized String Convenience |
var updatingLocationText: String { |
return NSLocalizedString("Location manager updating location", comment: "Inform user the location manager is updating location") |
} |
var notUpdatingLocationText: String { |
return NSLocalizedString("Location manager not updating location", comment: "Inform user the location manager is not updating location") |
} |
var deactivatingText: String { |
return NSLocalizedString("Watch is started to disconnect", comment: "Inform user the location manager is going to be paused") |
} |
var deactivatedText: String { |
return NSLocalizedString("Watch is not connected to phone", comment: "Inform user the location manager is temporarily paused") |
} |
var startButtonTitle: String { |
return NSLocalizedString("Start updating location", comment: "Pressing this button will start updating location") |
} |
var stopButtonTitle: String { |
return NSLocalizedString("Stop updating location", comment: "Pressing this button will stop updating location") |
} |
var locationBatchSizeTitleText: String { |
return NSLocalizedString("Location batch size:", comment: "Informs the user how many locations have been received since last batch push to the watch") |
} |
var receivedLocationCountTitleText: String { |
return NSLocalizedString("Total locations received:", comment: "Informs the user how many locations have been received since starting the app") |
} |
// MARK: Initialization |
required init?(coder aDecoder: NSCoder) { |
super.init(coder: aDecoder) |
commonInit() |
} |
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { |
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) |
commonInit() |
} |
/** |
Responds to the button press by either starting or stopping location updates |
depending on the current state. |
*/ |
/*@IBAction func startStopUpdatingLocation(sender: AnyObject) { |
if isUpdatingLocation.isEqual(true) { |
stopUpdatingLocation(commandedFromPhone: true) |
} |
else { |
startUpdatingLocationAllowingBackground(commandedFromPhone: true) |
} |
}*/ |
func startStopUpdatingLocation() { |
if isUpdatingLocation { |
stopUpdatingLocation(commandedFromPhone: true) |
} |
else { |
startUpdatingLocationAllowingBackground(commandedFromPhone: true) |
} |
} |
/** |
Sets the delegates and activate the `WCSession`. |
The `WCSession` needs to be activated in the init methods so that when the |
app is launched into the background when it wasn't previously running, the |
session can still be activated allowing communication between the watch and |
the phone. Activating the session in the `viewDidLoad()` method wont suffice |
since the `viewDidLoad()` method will not be called if the app is launched |
into the background. |
*/ |
func commonInit() { |
// Initialize the `WCSession` and the `CLLocationManager`. |
session.delegate = self |
session.activate() |
manager.delegate = self |
} |
override func viewDidLoad() { |
super.viewDidLoad() |
locationBatchSizeTitleLabel.text = locationBatchSizeTitleText |
receivedLocationCountTitleLabel.text = receivedLocationCountTitleText |
startStopUpdatingLocationButton.addTarget(self, action: #selector(startStopUpdatingLocation), for: UIControlEvents.touchUpInside) |
} |
/** |
Starts updating location and allows the app to receive background location |
updates. |
This method also sets the view into a state that lets the user know that |
the manager has started updating location, as well as starts the batch timer |
for sending location counts to the watch. |
Use `commandedFromPhone` to determine whether or not to call `requestWhenInUseAuthorization()`. |
If this method was called due to a command from the watch, the watch should |
be responsible for requesting authorization, and therefore this method |
should not request authorization. This ensures that the authorization prompt |
will come from the device that the user is currently interacting with. |
*/ |
func startUpdatingLocationAllowingBackground(commandedFromPhone: Bool) { |
isUpdatingLocation = true |
// When commanding from the phone, request authorization and inform the watch app of the state change. |
if commandedFromPhone { |
manager.requestWhenInUseAuthorization() |
do { |
try session.updateApplicationContext([ |
MessageKey.stateUpdate.rawValue: isUpdatingLocation |
]) |
} |
catch let error as NSError { |
print("Error when updating application context \(error.localizedDescription).") |
} |
} |
manager.allowsBackgroundLocationUpdates = true |
manager.startUpdatingLocation() |
sessionMessageTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(PotlocViewController.sendLocationCount), userInfo: nil, repeats: true) |
managerStatusLabel.text = updatingLocationText |
startStopUpdatingLocationButton.setTitle(stopButtonTitle, for: [.selected,.disabled,.application,.focused,.highlighted, .reserved]) // This should be normal but it can't be selected |
} |
/** |
Informs the manager to stop updating location, invalidates the timer, and |
updates the view. |
If the command comes from the phone, this method sends a state update to |
the watch to inform the watch that location updates have stopped. |
*/ |
func stopUpdatingLocation(commandedFromPhone: Bool) { |
isUpdatingLocation = false |
/* |
When commanding from the phone, request authorization and inform the |
watch app of the state change. |
*/ |
if commandedFromPhone { |
do { |
try session.updateApplicationContext([ |
MessageKey.stateUpdate.rawValue: isUpdatingLocation |
]) |
} |
catch let error as NSError { |
print("Error when updating application context \(error.localizedDescription)") |
} |
} |
manager.stopUpdatingLocation() |
manager.allowsBackgroundLocationUpdates = false |
sessionMessageTimer.invalidate() |
managerStatusLabel.text = notUpdatingLocationText |
startStopUpdatingLocationButton.setTitle(startButtonTitle, for: [.selected,.disabled,.application,.focused,.highlighted, .reserved]) // This should be normal but it can't be selected |
} |
/** |
On the receipt of a message, check for expected commands. |
On a `startUpdatingLocation` command, inform the manager to start updating |
location, and start a repeating 5 second timer that sends the cumulative |
location count to the watch. |
On a `stopUpdatingLocation` command, inform the manager to stop updating |
location, and stop the repeating timer. |
*/ |
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Swift.Void) { |
guard let messageCommandString = message[MessageKey.command.rawValue] as? String else { return } |
guard let messageCommand = MessageCommand(rawValue: messageCommandString) else { |
print("Unknown command \(messageCommandString).") |
return |
} |
DispatchQueue.main.async { |
switch messageCommand { |
case .startUpdatingLocation: |
self.startUpdatingLocationAllowingBackground(commandedFromPhone: false) |
replyHandler([ |
MessageKey.acknowledge.rawValue: messageCommand.rawValue as AnyObject |
]) |
case .stopUpdatingLocation: |
self.stopUpdatingLocation(commandedFromPhone: false) |
replyHandler([ |
MessageKey.acknowledge.rawValue: messageCommand.rawValue as AnyObject |
]) |
case .sendLocationStatus: |
replyHandler([ |
MessageKey.acknowledge.rawValue: self.isUpdatingLocation |
]) |
} |
} |
} |
/** |
This determines whether the phone is actively connected to the watch. |
If the activationState is active, do nothing. If the activation state is inactive, |
temporarily disable location streaming by modifying the UI. |
*/ |
@available(iOS 9.3, *) |
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { |
DispatchQueue.main.async { |
if activationState == .notActivated || activationState == .inactive { |
self.managerStatusLabel.text = self.deactivatedText |
} |
} |
} |
@available(iOS 9.3, *) |
func sessionDidBecomeInactive(_ session: WCSession) { |
managerStatusLabel.text = self.deactivatingText |
} |
@available(iOS 9.3, *) |
func sessionDidDeactivate(_ session: WCSession) { |
managerStatusLabel.text = self.deactivatingText |
} |
@available(iOS 9.0, *) |
func sessionWatchStateDidChange(_ session: WCSession) { |
} |
/** |
Send the current cumulative location to the watch and reset the batch |
count to zero. |
*/ |
func sendLocationCount() { |
do { |
try self.session.updateApplicationContext([ |
MessageKey.locationCount.rawValue: String(self.receivedLocationCount) as AnyObject |
]) |
locationBatchCount = 0 |
locationBatchSizeLabel.text = String(locationBatchCount) |
} |
catch let error as NSError { |
print("Error when updating application context \(error).") |
} |
} |
/** |
Increases that location count by the number of locations received by the |
manager. Updates the batch count with the added locations. |
*/ |
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { |
receivedLocationCount = receivedLocationCount + locations.count |
locationBatchCount = locationBatchCount + locations.count |
locationBatchSizeLabel.text = String(locationBatchCount) |
receivedLocationCountLabel.text = String(receivedLocationCount) |
} |
/// Log any errors to the console. |
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { |
print("Error occured: \(error.localizedDescription).") |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-10-04