Editor’s Note: This is part three 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 third part of our three part series we’ll be completing our weather alerts application started in Part One. A warning! The code examples that follow in this post build upon the code covered in both Part 1 and Part 2.
HAMweather
We’re going to make use of the HAMweather service for our weather data. There are a variety of weather services that provide rich APIs for obtaining all manner of weather-related data. Here’s just a few of them:
We chose HAMweather over the others solely on the fact that it provided weather advisory access in its free offering. As a bonus it comes with a nice iOS SDK framework for dealing with its weather API named Aeris.
Signing Up for HAMweather
To get started with HAMweather and Aeris, proceed on over to the signup page and select Aeris API – Developer Free then scroll down and enter the required user information. Of particular interest will be selection of your domain name: if you plan on using the Aeris SDK on the server-side choose the domain under which your servers are registered. In our case, we chose iachieved.it
.
Click Next to agree to the user agreement and then Login to login with your newly created username and password. Once you’ve logged in you’re going to want to proceed directly to managing your subscriptions by clicking on the Account tab.
Click on Aeris API – Application/Website Registration to register the application and generate a consumer ID and secret. Pay particular attention to your Application Domain/Identifier field, this needs to match your iOS application bundle identifier, which uses reverse DNS notation. iAchieved.it’s bundle identifier is it.iachieved.weatheralerts
, but yours will be based upon your reverse DNS.
Click the REGISTER button at the bottom of the page to register the application and get your application keys!
Now we need to add Aeris API keys to our application. We’re using our ApiKeys.plist
file to maintain all of our API keys, and have referred to the Aeris consumer ID and secret as AERIS_CONSUMER_ID
and AERIS_CONSUMER_SECRET
.
If you have a confused look on your face as to why we added these keys to a property list file, go back to Part One and our tutorial on using property lists for API key management.
Obtaining the iOS SDK
We’ll be using the new Aeris 2.0 SDK for iOS, which can be obtained from Github. Download the SDK 2.0 Beta 1 and unzip to a folder.
Add the Aeris.Framework
to your project by dragging and dropping it into your weatheralerts
project navigator. Ensure that the Copy items if needed and Create groups are selected, and that your weatheralerts
target is checked. For our purposes we only need Aeris.Framework
, but if we wanted to begin leveraging other features of Aeris we would add some of the other frameworks.
Aeris also requires using the link flag -ObjC
. To set the flag go to your project’s Build Settings and type Other Linker in the search box. You will see Other Linker Flags in the Linking section. Add -ObjC
:
Adding AFNetworking
Aeris makes uses of the popular AFNetworking networking library for iOS, so we will need to add it to our application first. We’re going to grab the 2.5.0 version, so go to Github and click on the Download Zip button. Note: We could have used Cocoapods to manage the installation of AFNetworking, but there’s additional setup required to get Cocoapods going, and quite frankly, I don’t like the way it takes over my Xcode project organization.
Drag and drop both the AFNetworking
and UIKit+AFNetworking
(Aeris makes use of AFNetworkActivityIndicatorManager
class) folders into your project, again ensuring that Copy items if needed and Create groups are selected and that the files are added to your target.
Using Aeris
Now that we have all of the prerequisites out of the way we can get down to business with actually using Aeris. The first step in accessing the Aeris SDK routines in a Swift-based project will be to import its header files in our application’s bridging header. Add the following to your bridging header (which should be named bridingHeader.h
):
[objc]
#import <Aeris/Aeris.h>
[/objc]
The complete bridgingHeader.h
file should look something like this now:
[objc]
#ifndef weatheralerts_bridgingHeader_h
#define weatheralerts_bridgingHeader_h
#import <Parse/Parse.h>
#import <Aeris/Aeris.h>
#endif
[/objc]
We’ll now turn to the WeatherServiceController
class created in Part 2 of our tutorial. Find the init()
routine and ensure it looks like the following:
[objc]
override init() {
super.init()
let aerisConsumerId = valueForAPIKey(keyname: "AERIS_CONSUMER_ID")
let aerisConsumerSecret = valueForAPIKey(keyname: "AERIS_CONSUMER_SECRET")
AerisEngine.engineWithKey(aerisConsumerId, secret: aerisConsumerSecret)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "locationAvailable:",
name: "LOCATION_AVAILABLE",
object: nil)
}
[/objc]
AerisEngine.engineWithKey()
initializes our Aeris framework with the consumer ID and secret that we received when registering our application.
The Aeris SDK employs a notion of loaders that are used to obtain information from the HAMweather service. To obtain a list of advisories in our current location we first have to obtain an Aeris place and then use an advisory loader to retrieve the advisories (if any) for our place.
To create a place, or, AWFPlace
, we provide city, state, and country information like so:
[objc]
let place = AWFPlace(city: city, state: state, country: country)
[/objc]
We can at this point load the advisories for our place using an AWFAdvisoriesLoader
.
[objc]
let advisoryLoader = AWFAdvisoriesLoader()
advisoryLoader.getAdvisoriesForPlace(place, options: nil) { (advisories, e) -> Void in
}
[/objc]
The getAdvisoriesForPlace
function takes our place and returns a list (array) of advisories currently issued for it. Let’s put this all together and place in our WeatherServiceController
‘s locationAvailable
function:
[objc]
func locationAvailable(notification:NSNotification) -> Void {
let userInfo = notification.userInfo as Dictionary<String,String>
println("WeatherService: Location available \(userInfo)")
let city = userInfo["city"]!
let state = userInfo["state"]!
let country = userInfo["country"]!
let place = AWFPlace(city: city, state: state, country: country)
let advisoryLoader = AWFAdvisoriesLoader()
advisoryLoader.getAdvisoriesForPlace(place, options: nil) { (advisories, e) -> Void in
if let error = e {
println("Error: \(error.localizedDescription)")
} else {
// Take the last advisory
if let advisory = advisories.last as? AWFAdvisory {
let userInfo = [
"location": city + ", " + state,
"name": advisory.name,
"body": advisory.body
]
NSNotificationCenter.defaultCenter().postNotificationName("ADVISORY_AVAILABLE",
object: nil,
userInfo: userInfo)
} else {
println("no advisories")
}
}
}
}
[/objc]
Now, when our WeatherServiceController
receives a new location from our CoreLocationController
(via a NSNotification
labeled LOCATION_AVAILABLE
), it will unpack this information from the userInfo
dictionary, create an AWFPlace
, and then use the AWFAdvisoriesLoader
to load any advisories for the area.
Let’s take a look at what we’re doing with the advisories
array. For simplicity, and to not barrage the user with push notifications, we’re going to take the last advisory in any list of advisories returned to us. The expression if let advisory = advisories.last as? AWFAdvisory
is our use of Swift optionals to evaluate the if
block to true if we have an advisory. If we do have an advisory then we create a userInfo
dictionary containing the location of the advisory, the advisory name, and an advisory body which contains additional information. This information is then posted to the NSNotificationCenter
with the name ADVISORY_AVAILABLE
.
We’ve only scratched the surface in what is possible with the Aeris SDK. For more information on using Aeris, see the Getting Started documentation.
Pushing the Advisory
If you recall from Part One of our tutorial we created a class PushNotificationController
to be responsible for managing our relationship with the Parse framework. Let’s return to our PushNotificationController
class and update its init
routine to handle listening for an ADVISORY_AVAILABLE
notification.
The full init()
function for PushNotificationController
should now look like:
[objc]
override init() {
super.init()
let parseApplicationId = valueForAPIKey(keyname: "PARSE_APPLICATION_ID")
let parseClientKey = valueForAPIKey(keyname: "PARSE_CLIENT_KEY")
Parse.setApplicationId(parseApplicationId, clientKey: parseClientKey)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "advisoryAvailable:",
name: "ADVISORY_AVAILABLE",
object: nil)
}
[/objc]
Now let’s write our advisoryAvailable:
routine:
[objc]
func advisoryAvailable(notification:NSNotification) -> Void {
println("advisoryAvailable")
// Get our advisory name
let userInfo = notification.userInfo as Dictionary<String,String>
let advisoryLocation = userInfo["location"]!
let advisoryName = userInfo["name"]!
let pushMessage = "Weather Advisory for \(advisoryLocation): \(advisoryName)"
// Create our Installation query
let token = PFInstallation.currentInstallation().deviceToken
let pushQuery:PFQuery = PFInstallation.query()
pushQuery.whereKey("deviceToken", equalTo:token)
// Send push notification to query
let pushNotification:PFPush = PFPush()
pushNotification.setQuery(pushQuery)
pushNotification.setData([
"sound":"alert.caf",
"alert":pushMessage
])
pushNotification.sendPushInBackgroundWithBlock({ (succeeded,e) -> Void in
if succeeded {
println("Push message to query in background succeeded")
}
if let error = e {
println("Error: \(error.localizedDescription)")
}
})
}
[/objc]
Breaking this down, we:
- unpack our advisory information from the
NSNotification
- build a Parse
PFQuery
targeted for our device - use Parse to send the push notification
This code uses Parse’s client push capability which must be enabled for your Parse application. Go to your application in Parse and to the Settings – Parse page and enable client push.
Warning!Failure to enable client push in the Parse portal will result in your push notification failing!
We should be able to now run our application and using a combination of Wunderground and Xcode simulator find a location that we know has a pending advisory. Simply look at the current weather advisories, find a city in one, and then use Google to look up the GPS coordinates. As of today there is a Coastal Flood Advisory for Franklin, Louisiana, so we’ll put it’s GPS coordinates in our Waypoints.gpx
file.
Remember to put a minus sign for the longitude coordinate if you want the Western hemisphere!
Putting everything together and running our application, we get a nice push notification about a coastal flood advisory in Franklin, Louisiana.
Adding Sound to our Push Notification
Push notifications should typically grab your attention, especially ones for weather alerts! If you recall our push notification data is set as follows:
[objc]
pushNotification.setData([
"sound":"alert.caf",
"alert":pushMessage
])
[/objc]
The sound
data specifies a sound resource to play back with the receipt of the push notification. This file has to be included in your application resources obviously to be located and played. So grab our alert.caf file and add it to your project. Then, ensure that it is added as a resource by going to your application target and selecting the Build Phases page and adding alert.caf
in the Copy Bundle Resources section.
Bitbucket Xcode Project
The work done thus far is available on Bitbucket on a branch named part3. 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 HAMweather 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 and HAMweather API keys - Create and configure your own provisioning profile
Additional Thoughts
Our application is pretty basic. So basic, in fact, there’s no user interface. No home location we can set, no weather forecast to look at, and we can’t even choose which types of advisories we want to receive. Honestly, who needs coastal flood advisories sent to them? Well, other than those on the coast. There’s plenty we can do with the basic application framework developed, and now is where the fun can really begin.
Unfortunately, there is a bit of a flaw in our application design. What happens if an advisory is issued for an area after the user has moved into area? Oh dear. Well, perhaps we can run our application in the background and periodically wake up and check for advisories? Nope. Could we increase the frequency of receiving location updates in the background? Sure, if you wanted Apple to reject your application outright.
This is where it can become all too obvious that mobile applications that provide up-to-date information feeds typically require some type of cloud application to push those feeds to users. Taking a look at our demo application the issue is that something needs to be periodically checking whether or not there are new advisories for the user’s last known location.
Never fear. We’re going to expand this tutorial series to include utilizing Parse’s Cloud Code feature to run background jobs to update a user if there is a new advisory for their last location. Stay tuned!