Editor’s Note: This is part two in a three part series. The goal in this series is to develop a fully functional (and useful) iOS application that is capable of pushing weather alerts based on your location to your iOS device. The series is broken down as follows:
- Part One – Developing a Push Notification-capable application with Parse and Swift
- Part Two – Using CoreLocation with Swift
- Part Three – Integrating in a Weather Service API
In this second part of our three part series we’ll be expanding on our weather alerts application started in Part One. A warning! The code examples that follow build upon the code in Part 1.
CoreLocation
CoreLocation is the iOS framework which provides location-based information to your application. By location we mean either the phone’s location in a GPS-coordinate space, or we can mean in reference to beacon devices. In this tutorial we are interested in the GPS location of the device.
Before we get started using CoreLocation, it is important to note that Apple has raised the bar in terms of how resource-intensive your application is with regards to using location-based services. Our application, Lewis and Clark, made use of CoreLocation to send frequent and accurate GPS locations to determine whether a user was in a new county or state. Apple had approved the application through several iterations and then rejected the application on the grounds it was requesting “navigation-level” GPS coordinates and was not a navigation application. When writing an application that makes use of location updating strive to only request the bare minimum accuracy necessary for your application to work correctly.
Okay, let’s get started. In your weatheralerts Xcode project, create a new Swift file called CoreLocationController.swift
. If you don’t have the project you can get the version from here. Make sure and read the Part One post to obtain API keys for Parse and add them to your project.
CoreLocation uses the delegation pattern, so our class needs to declare that is implements the CLLocationManagerDelegate
protocol. If you are unfamiliar with using the delegation pattern in iOS, see the following excellent tutorials:
Back to the code! Here is our initial content for CoreLocationController.swift
:
1 2 3 4 5 |
import Foundation import CoreLocation class CoreLocationController : NSObject, CLLocationManagerDelegate { } |
Next let’s declare a member variable locationManager
and configure it in the init()
method as follows (this code goes in your CoreLocationManager
class!):
1 2 3 4 5 6 7 |
var locationManager:CLLocationManager = CLLocationManager() override init() { super.init() self.locationManager.delegate = self self.locationManager.requestAlwaysAuthorization() } |
Note: To declare Swift classes as delegates you must first inherit from NSObject
. See Stackoverflow for a good answer as to why.
Before we forget, go to the AppDelegate
and add:
1 |
var coreLocationController:CoreLocationController? |
as a member variable after the pushNotificationController
declaration, and in the didFinishLaunchingWithOptions
function add:
1 |
self.coreLocationController = CoreLocationController() |
after the self.pushNotificationController = PushNotificationController()
line.
Delegate Methods
There are a number of CoreLocation delegate methods we need to implement in our CoreLocationController
class, but the first we will implement is locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus)
. iOS 8 introduced changes to how apps are authorized to use CoreLocation, and we want to capture when our app moves from a given authorization state to the Authorized
state. To capture that transition we implement the delegate method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) { println("didChangeAuthorizationStatus") switch status { case .NotDetermined: println(".NotDetermined") break case .Authorized: println(".Authorized") self.locationManager.startUpdating() break case .Denied: println(".Denied") break default: println("Unhandled authorization status") break } } |
If we tried to compile and run this code as is you would notice that no authorization request is made. iOS 8 requires you to not only request authorization status (with the code requestAlwaysAuthorization
function), but also provide a description as to why you are requesting a given authorization. The text description is given by the key NSLocationAlwaysUsageDescription
(when requesting always authorization, which is used to receive location updates while the app is in the background) in your Info.plist
. To add it, edit your Info.plist
in Xcode. I prefer to right-click in the Info.plist
editor window and Show Raw Keys/Values before inserting a new key.
In this example we’ve added our key NSLocationAlwaysUsageDescription
and the reason we give is “Your location is used to send you timely weather alerts in your immediate area.”
Run the application again and you should see:
Press Allow and your application should switch to an .Authorized
status. If you have the same println
statements in your code as above you will see in the console log:
1 2 |
didChangeAuthorizationStatus .Authorized |
Notice that when our app changed to .Authorized
we call self.locationManager.startUpdating()
. startUpdating()
is a method on CLLocationManager
which will be responsible for obtaining GPS coordinates and delivering them to us. And since this is a delegate pattern, we need to implement the function that will be called when CLLocationManager
has GPS coordinates: locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!)
.
Here’s our initial implementation of this delegate function:
1 2 3 4 5 6 7 |
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) { let location = locations.last as CLLocation println("didUpdateLocations: \(location.coordinate.latitude), \(location.coordinate.longitude)") } |
Run the application again and you should begin receiving a stream of locations. Too many locations as a matter of fact, and they are all likely right on the same location. We want to fine tune how many location updates we receive, as well as our relax our accuracy requirements.
Consider our use-case, receiving weather alerts relative to our location. Does it matter if our GPS location is accurate down to a meter? No. Chances are a weather alert in your area is going to be the same as in your next-door neighbor’s area, i.e., you are in the same “area”. We can greatly reduce the demands of our application without sacrificing accuracy by relaxing our GPS accuracy to a kilometer. Moreover, we don’t need any additional updates to our location unless we move significantly, say 3 kilometers. This can be achieved by adding the following code after self.locationManager.delegate = self
in our init()
routine.
1 2 |
self.locationManager.distanceFilter = 3000 // Must move at least 3km self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer // Accurate within a kilometer |
The location reported by our phone is the green dot. Because our accuracy is set to one kilometer we could actually be anywhere in the blue circle. Again, we don’t really care. On the edge of the blue circle, right in the center, etc., its all the same for a weather alert. The red circle represents our distance filter, that is, the distance we have to move before we will receive another update. As long as our green dot stays within the red circle, we won’t receive an update from CoreLocation, which again is fine for our purposes.
Note: In our application we might even be better served by significant location change updates from iOS, but for our purposes we’ll use standard location updating.
Reverse Geocoding
We now have the basics for receiving location updates whenever our phone moves approximately 3 kilometers from its previous location, and our location is accurate within a kilometer. Our weather API will require us to pass city, state, and country information to provide weather alerts. We can obtain this information by using the reverse geocoding feature of the CLGeocoder
class. Reverse geocoding relies on databases to take GPS coordinates and return the “address” of that location. iOS encapsulates the returned data in a CLPlacemark
object which has various attributes we can extract as follows (this code goes in our didUpdateLocations
delegate function):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
let geocoder = CLGeocoder() geocoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, e) -> Void in if let error = e { println("Error: \(e.localizedDescription)") } else { let placemark = placemarks.last as CLPlacemark let userInfo = [ "city": placemark.locality, "state": placemark.administrativeArea, "country": placemark.country ] println("Location: \(userInfo)") } }) |
We’ll note that the call reverseGeocodeLocation
returns immediately, and it is only after the reverse geocoding results are available (a network request is made to Apple servers to look up the data) is the completionHandler
block called. Heed the warning in Apple’s documentation when using reverse geocoding! Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value kCLErrorNetwork to your completion handler.
Simulating Movement
At some point we’re going to need to test our application and ensure that current weather alerts are recognized and displayed to the user. I live in North Texas, and while we do get severe weather from time to time (tornados anyone?), we don’t have severe weather every day. So how are we going to test our application? Simple. We will take a look at Wunderground’s severe weather alert map, pick a city within an alert area, look up its GPS coordinates, and give that information to Xcode to simulate for us.
Before we go into the details of getting the GPS address of a city of interest, let’s start with creating a GPX file in Xcode for GPS-location simulation. To create a GPX file use Xcode File – New – File and select iOS Resource – GPX File
Name the file Waypoints
. Edit the file in Xcode and delete the default waypoint (the lone <wpt/> element) and then add:
1 |
<wpt lat="42.904722" lon="-78.849444"></wpt> |
This coordinate is in Buffalo, New York, which, as of this writing, has a winter weather alert. Run the application and then use Xcode to simulate the location based upon the content of the Waypoints
GPX file. This is done within Xcode, on the bar above the Debug Area notice the location arrow:
You should see after triggering the location simulator to use Waypoints
:
1 2 |
didUpdateLocations: 42.904722, -78.849444 Location: [country: United States, city: Buffalo, state: NY] |
Posting Our Location
I am a big fan of the notification model in iOS application development. Rather than add hooks across classes with references to objects, or create a new delegate protocol, simply post a notification to the notification center and let anyone who is interested in the data subscribe to it (sometimes referred to as pub-sub).
We will eventually write a class that handles taking our location and uses a weather service API to look up whether there are any active alerts. That class is going to listen for an event that the CoreLocationController
will publish. For simplicity we’ll call it the LOCATION_AVAILABLE
event. Here’s how we use the notification center to post it. After extracting the CLPlacemark
data into a userInfo
dictionary, add the following line:
1 |
NSNotificationCenter.defaultCenter().postNotificationName("LOCATION_AVAILABLE", object: nil, userInfo: userInfo) |
Let’s go ahead and create the class that will receive and handle the notification. Create a new file called WeatherServiceController
and implement the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class WeatherServiceController : NSObject { override init() { super.init() NSNotificationCenter.defaultCenter().addObserver(self, selector: "locationAvailable:", name: "LOCATION_AVAILABLE", object: nil) } func locationAvailable(notification:NSNotification) -> Void { let userInfo = notification.userInfo as Dictionary<String,String> println("WeatherService: Location available \(userInfo)") } } |
Note: WeatherServiceController
must inherit from NSObject
to make use of the notification center. Failure to derive the class from NSObject
will result in the following type of trap when the notification is posted by the CoreLocationController
:
1 2 |
2014-11-19 21:54:26.405 weatheralerts[914:177667] *** NSForwarding: warning: object 0x1563db00 of class 'weatheralerts.WeatherServiceController' does not implement methodSignatureForSelector: -- trouble ahead Unrecognized selector -[weatheralerts.WeatherServiceController locationAvailable:] |
Before trying to run the again, remember, we have to create an instance of our WeatherServiceController
. Head back to the AppDelegate
and add:
1 |
var weatherServiceController:WeatherServiceController? |
to your member variable declarations and:
1 |
self.weatherServiceController = WeatherServiceController() |
in your didFinishLaunchingWithOptions
function. At this point your AppDelegate
should look a bit something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var pushNotificationController:PushNotificationController? var coreLocationController:CoreLocationController? var weatherServiceController:WeatherServiceController? func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { self.pushNotificationController = PushNotificationController() self.coreLocationController = CoreLocationController() self.weatherServiceController = WeatherServiceController() ... |
Running the code again and simulating to Buffalo, New York:
1 2 3 |
didUpdateLocations: 42.904722, -78.849444 Location: [country: United States, city: Buffalo, state: NY] WeatherService: Location available [country: United States, city: Buffalo, state: NY] |
Receiving Events in the Background
If you’ve been working along and put the application in the background, you’ll notice no updates are received! That sort of defeats the purpose, so we have to explicitly add the capability to receive location updates in the background. Go to your application target and navigate to the Capabilities page and scroll down to Background Modes. Turn background modes on and select Location updates
Bitbucket Xcode Project
The work done thus far is available on Bitbucket on a branch named part2. To use the project you can directly download the zip file and open the enclosed Xcode project. There are a few steps you’ll need to complete to use the project:
- Sign up for Parse and obtain your own API keys
- Change the Bundle Identifier to reflect your organization (that is, change
it.iachieved.
tocom.yourcompany
) - Create and add your own
ApiKeys.plist
file and use your Parse API keys - Create and configure your own provisioning profile
Part 3
In part three we’ll be adding the final touches to the application which will be passing the reverse geocoded location to our weather service API. This API will in turn provide us with information about weather alerts for the area, and if any are available we will provide that information to Parse to receive a push notification.