Editor’s Note: This is a tutorial on how to use iOS 8.2 with Xcode 6.2 to develop an Apple Watch app. It is not an exhaustive overview of WatchKit. I recommend reading the following first in this order:
By the time you’ve finished reading these tutorials you should have a basic understanding of the architecture of a WatchKit app and the application lifecycle of a WatchKit Extension (the app component that runs on your iPhone).
While I’ve tried to provide plenty of step-by-step detail, this tutorial does make an assumption that you have a basic understanding of iOS application development and are comfortable navigating in Xcode. You will of course require an iOS developer’s account to obtain Xcode 6.2 and the iOS 8.2 simulator included.
A Current Conditions WatchKit App
Starter Application
It should have been clear from reading the Apple WatchKit Programming Guide that your Watch app is an extension of an existing iPhone application. That is, there is no such thing as an Apple Watch-only application. One of the examples given on Apple’s website is that of an American Airlines watch app that shows flight updates, boarding notifications, etc. The American Airlines watch app cannot be used without downloading and installing the iPhone app, as that is where the code for the Watch app will reside.
Rather than walk through how to create an application from scratch we’ll begin with a starter application and add an Apple Watch app to it. Ours is a basic weather app that provides a brief look at the weather in your current location. To keep things simple we’ll provide:
- the name of the current location (for example, New Orleans, LA)
- a weather icon depicting the current conditions
- a brief description of the current conditions
- the current temperature
Even though this is a simple app in its functionality, it does provide a good example of how we will leverage third-party SDKs and the CoreLocation framework to provide location-aware weather information to an Apple Watch app. Warning: If you thought this was going to be a “How do I write a simple WatchKit app in 10 minutes?” tutorial, try out the Bitcoin Price Tracker app first. Ours is a bit more involved.
Download the starter application from BitBucket and open the project in Xcode.
To get started with the app you’ll need to change your aerisweather target bundle identifier. In this example we’ve changed it to com.yourcompany.aerisweather
.
We will be using the Aeris Weather API from HAMweather.com, so you will need to register for the service and create some API keys. You may want to review our previous post on signing up.
Create a new application in the HAMweather portal by filling out your Application Name and Application Domain/Identifier.
Click Register to register your application and receive your API keys.
Configure your keys by creating a property list file called ApiKeys.plist
. This can easily be done through Xcode with File – New – File selecting the iOS Resource item and then Property List. Again, name it ApiKeys
.
If Xcode has two ApiKeys.plist
files listed in the project navigator, select one of them, right-click, and Delete and choose Remove Reference (not Trash!).
Add two items to the property list file,AERIS_CLIENT_ID
and AERIS_APP_SECRET
, and set them to the values of the keys that were created when you registered your application in the Aeris portal.
Stop and build the iOS application at this point in the iOS simulator for either the iPhone 5, 5s, 6, or 6 Plus. If Xcode complains about the lack of a provisioning profile click the Fix Issue button to generate an appropriate provisioning profile.
The simulator should prompt you requesting location access. Click Allow. Hopefully you see something like this in your simulator.
Simulating Location
We are using significant location change monitoring which relies primarily on recognizing locations of Wifi networks and cell towers rather than GPS. We use this rather than GPS because we don’t require the same level of accuracy for providing the current weather. Of course in large urban areas where cities blend together your app may show you to be in Dallas when in reality you are in Richardson. To simulate significant location changes in the simulator select the Debug menu and choose Location – Freeway Drive. At this point you can stop the application. As long as the Freeway Drive location simulation is running we will have data for us in our Watch app.
Note: If the app is not receiving location updates (and correspondingly you see a screen that has default label text), go to Hardware – Location and select None. Then, go back to Hardware – Location and select Freeway Drive.
Adding the Watch App
Let’s add a watch app to our weather app! I assume you have the aerisweather project open and you’ve already added API keys for Aeris and have run the starter app in the simulator. If you haven’t, do so, the remainder of the tutorial builds upon these steps.
Now, to add a Watch App target in Xcode go to File – New – Target and select Apple Watch.
Take a moment to note what the Watch App template does: This template builds a Watch App with an associated WatchKit app extension. It bears repeating that there are two items about to be added: the watch app itself which runs on the Apple Watch and the watchkit app extension which runs on the iPhone.
Click Next. When choosing your target options make sure for this tutorial to uncheck Include Notification Scene and Include Glance Scene. We will not be including either a notification or glance scene in this tutorial. Also ensure your Language is set to Swift. Click Finish.
Notice on this screen also there is a new bundle identifier. If you used com.yourcompany.aerisweather
for your bundle identifier above, the new identifier for the watch app will be com.yourcompany.aerisweather.watchapp
. As you will see below there is also a third bundle identifier for the app extension named com.yourcompany.aerisweather.watchkitextension
.
Again, two new targets have been added, along with a new scheme. The targets are the watch app extension and watch app, and the scheme is aerisweather Watch App.
When first working with WatchKit it was confusing understanding how to simulate the Watch. The answer is by using the new scheme Xcode created, in our case it is named aerisweather Watch App. You must use the simulator for now; that is, you cannot run the app and app extension on your physical iPhone and simulate the watch.
Let’s lay out our watch interface using the Xcode storyboard created for us. Go to the file Interface.storyboard in the aerisweather Watch App folder. Like our iPhone app we will have a label for our last known location, a weather icon, current conditions label, temperature label, and at the bottom a last updated string. Here is our layout:
Laying items out on a watch interface is a bit easier than for an iOS device. For one reason we don’t have to use constraints! There are a few things to note. First, our layout was created by adding, in this order:
- Label with a horizontal position of center
- Group
- Image inside the group with a horizontal position of Left and size of 64×64
- Label inside the group with a horizontal position of Right
- Label with a horizontal position of center
- Label with a horizontal position of center
Second, we are using Avenir Next for our font, and have set the currentTemperature label to 30-pt, and the lastUpdated label to 10-pt. The remaining labels are 16-pt.
We now need to wire our elements up to the InterfaceController
. Notice that the controller is in our extension and not in the Watch app itself! But if you click on Interface.storyboard and then the Assistant Editor icon (the joined circles), Xcode will place the storyboard and controller side-by-side in the editor.
Just as with building iOS applications, simply CTRL-click from the UI element and drag over to the InterfaceController class to insert the Outlet. To be consistent with our iPhone interface we’ll name these:
currentLocationLabel
currentWeatherIcon
currentTemperatureLabel
currentConditionsLabel
lastUpdatedLabel
After adding our IBOutlets
1 2 3 4 5 |
@IBOutlet weak var currentLocationLabel: WKInterfaceLabel! @IBOutlet weak var currentWeatherIcon: WKInterfaceImage! @IBOutlet weak var currentTemperatureLabel: WKInterfaceLabel! @IBOutlet weak var currentConditionsLabel: WKInterfaceLabel! @IBOutlet weak var lastUpdatedLabel: WKInterfaceLabel! |
It is possible to run the watch app at this point and see the labels displayed. To run the app select the aerisweather Watch App scheme and click Run. To see the Apple Watch display go to the simulator and in the Hardware menu find External Displays and select one of the two Apple Watch displays.
Note: I’ve frequently had trouble with the simulator and watch kit extensions running properly. If you see in your Xcode debug navigator waiting to attach
to your process and it never attaches, simply stop the application and run again. One more than one occasion I’ve had to restart the simulator altogether.
App Groups
To display the weather on the Apple Watch we’re going to need to fetch it from Aeris, but to fetch it from Aeris we’re going to need to obtain the last known location.
We need to stress that our app extension does not have access to iOS CoreLocation, thus it cannot retrieve the location and update the weather. Why? Because Apple warned us not to use it. From Apple’s WatchKit guidelines: “Avoid using technologies that request user permission, like Core Location. Using the technology from your WatchKit extension could involve displaying an unexpected prompt on the user’s iPhone the first time you make the request. Worse, it could happen at a time when the iPhone is in the user’s pocket and not visible.” So there you go.
To obtain the location we leverage sharing application data between our iOS application and our app extension. Select your project in the project navigator and select the aerisweather WatchKit Extension target. Then select the Capabilities page and scroll down to App Groups. Enable it and select groups.aerisweather
.
Now, let’s add some code! Open InterfaceController.swift
and add the following to the willActivate
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let url = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.aerisweather") let file = url!.URLByAppendingPathComponent("aerisweather.plist") if NSFileManager.defaultManager().fileExistsAtPath(file.path!) { let dict = NSDictionary(contentsOfURL: file) as Dictionary<String,AnyObject> let city = dict["city"]! as String let state = dict["state"]! as String let country = dict["country"]! as String let lastUpdatedAt = dict["timestamp"]! as NSDate println("Last location: \(city), \(state), at \(lastUpdatedAt)") } else { println("No last location") } |
Run your watch app again and you should see in your console log something like:
1 2 3 |
2014-11-29 11:14:51.067 aerisweather WatchKit Extension[16728:1012693] <aerisweather_WatchKit_Extension.InterfaceController: 0x7fa25141f1c0> init 2014-11-29 11:14:51.083 aerisweather WatchKit Extension[16728:1012693] <aerisweather_WatchKit_Extension.InterfaceController: 0x7fa25141f1c0> will activate Last location: Los Altos Hills, CA, at 2014-11-29 17:14:32 +0000 |
If the log instead says No last location
you haven’t launched and run your iOS application. Remember, our watch application (and its corresponding extension) does not run CoreLocation, so it cannot directly obtain the user’s location. Only the iOS application requests and receives location information.
What exactly did we just do? By enabling app groups we are allowing our iOS application and our watchkit extension to share information. When our iOS application receives a new location from CoreLocation it reverse-geocodes the coordinate and then writes that information to a file in the app group group.aerisweather
. When you launch your watch app it reads that information from the file in the app group. Check out the CoreLocationController
‘s didUpdateLocations
function to see where the information is being written.
Note that we must enable App Groups in each app that is granted permission to read and write in the shared group.aerisweather
group.
Adding Aeris
Now, let’s get to adding Aeris to our watch app. To be more precise, we are adding Aeris to the watchkit app extension, as no Aeris code runs on the watch itself. As such, and this is key, we need to register another application in the HAMweather.com portal. Because we will be executing Aeris routines inside an application with a different bundle ID (remember, our extension has a bundle identifier of com.yourcompany.aerisweather.watchkitextension
we need to register it.
Add your new secret to the ApiKeys.plist
file as AERIS_EXTAPP_SECRET
. In the property list you should now have three different keys: AERIS_CLIENT_ID
(which is shared by all the applications in your account), AERIS_APP_SECRET
and AERIS_EXTAPP_SECRET
.
Next we need to configure our Objective-C bridging header for the watchkit extension target. If you’ve been working with Swift and Objective-C in the same projects you should be accustomed to this step by now.
Set the bridging header to $(SRCROOT)/bridgingHeader.h
(you will see it is actually already a part of the overall project since we use it for our iOS application as well).
Now let’s add the Aeris.framework
itself. Select the aerisweather WatchKit Extension
target and then Build Phases. Click the disclosure triangle for Link Binary With Libraries and click the + icon. When presented with the Choose frameworks and libraries to add dialog, click Add Other. Locate the Aeris.framework item in your project directory, select it, and click Open.
We’ll be using the ApiKeys.swift
in the watch kit extension, so we need to add it to the compile sources. You should already be in the Build Phases panel for the watch kit extension, so click the disclosure triangle for Compile Sources and then click the + icon. Find the ApiKeys.swift
file, select it, and click Add.
Now, add the ApiKeys.plist to the Copy Bundle Resources list. In the same area, Build Settings, disclose the Copy Bundle Resources area, click +, and add the ApiKeys.plist
.
The Aeris framework also requires us to add -ObjC
to Other Linker Flags in the Build Settings (if you don’t you’ll get a nasty crash with -[__NSCFString awf_URLEncodedString]: unrecognized selector sent to instance
). Go ahead and add it.
Adding AFNetworking
We are going to compile our AFNetworking code into our target (rather than using a pre-compiled framework), so we need to add all of the required AFNetworking files to our Compile Sources. Go back to Compile Sources in the Build Settings panel for the extension target, click the + icon, and find the AFNetworking
folder. Select each of the .m
files in the AFNetworking
and UIKit+AFNetworking
folders and add them. This can be done quickly by holding down the Option key while selecting each file.
There are iOS routines that are not available to app extensions. In particular sharedApplication
is referenced in AFNetworking code and it is not available. If you tried to compile now, you will see an error like 'sharedApplication' is unavailable: not available on iOS (App Extension)
. The AFNetworking authors recognized this issue and added a flag AF_APP_EXTENSIONS
that can be defined to not call on unavailable routines. For an example see Github, and here is a screenshot of what the error looks like:
To set the AF_APP_EXTENSIONS
define go to your aerisweather WatchKit Extension Build Settings page and type preprocessor in the search box and locate the section headed Preprocessing. We will be building the Debug configuration in this tutorial so add AF_APP_EXTENSIONS=1
underneath the DEBUG=1
define.
Rebuilding at this point will still result in an error regarding iOS App Extensions and sharedApplication
because a routine inside the Aeris SDK uses AFNetworkActivitityIndicatorManager
. The offending routine in AFNetworkActivityIndicatorManager.m
is
1 2 3 |
- (void)updateNetworkActivityIndicatorVisibility { [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:[self isNetworkActivityIndicatorVisible]]; } |
To resolve the issue simply wrap the contents of the method with #if !defined(AF_APP_EXTENSIONS)
and #endif
:
1 2 3 4 5 |
- (void)updateNetworkActivityIndicatorVisibility { #if !defined(AF_APP_EXTENSIONS) [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:[self isNetworkActivityIndicatorVisible]]; #endif } |
One more file needs to be touched: UIAlertView+AFNetworking.m
. There are calls to UIAlertView
in the methods showAlertViewForTaskWithErrorOnCompletion
and showAlertViewForRequestOperationWithErrorOnCompletion
. Wrap both calls to the UIAlertView with #if !defined(AF_APP_EXTENSIONS)
and corresponding #endif
.
Take a moment to compile and run your watch app: there should be no unresolved errors at this point!
Updating willActivate
Now that we have everything configured properly for using Aeris, let’s fill out our routine to include the following in the willActivate
routine after we receive our location. This block of code goes immediately following the line println("Last location: \(city), \(state), at \(lastUpdatedAt)")
in InterfaceController.swift
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/* Initialize Aeris */ let aerisConsumerId = valueForAPIKey(keyname: "AERIS_CLIENT_ID") let aerisConsumerSecret = valueForAPIKey(keyname: "AERIS_EXTAPP_SECRET") AerisEngine.engineWithKey(aerisConsumerId, secret: aerisConsumerSecret) /* Use Aeris to obtain current weather for last known location */ let place = AWFPlace(city: city, state:state, country:country) let loader = AWFObservationsLoader() loader.getObservationForPlace(place, options: nil, completion: { (observations, error) -> Void in if observations.count > 0 { let observation = observations[0] as AWFObservation self.currentWeatherIcon.setImageNamed(observation.icon) self.currentConditionsLabel.setText(observation.weather) self.currentLocationLabel.setText("\(city), \(state)") self.currentTemperatureLabel.setText("\(observation.tempF)°") let dateFormatter = NSDateFormatter() let formattedDate = NSDateFormatter.localizedStringFromDate(lastUpdatedAt, dateStyle: .ShortStyle, timeStyle: .ShortStyle) self.lastUpdatedLabel.setText(formattedDate) } else { println("No observations") if let e = error { println("error: \(e)") } } }) |
The full willActivate
routine should now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
override func willActivate() { // This method is called when watch view controller is about to be visible to user super.willActivate() let url = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.aerisweather") let file = url!.URLByAppendingPathComponent("aerisweather.plist") if NSFileManager.defaultManager().fileExistsAtPath(file.path!) { let dict = NSDictionary(contentsOfURL: file) as Dictionary<String,AnyObject> let city = dict["city"]! as String let state = dict["state"]! as String let country = dict["country"]! as String let lastUpdatedAt = dict["timestamp"]! as NSDate println("Last location: \(city), \(state), at \(lastUpdatedAt)") /* Initialize Aeris */ let aerisConsumerId = valueForAPIKey(keyname: "AERIS_CLIENT_ID") let aerisConsumerSecret = valueForAPIKey(keyname: "AERIS_EXTAPP_SECRET") AerisEngine.engineWithKey(aerisConsumerId, secret: aerisConsumerSecret) /* Use Aeris to obtain current weather for last known location */ let place = AWFPlace(city: city, state:state, country:country) let loader = AWFObservationsLoader() loader.getObservationForPlace(place, options: nil, completion: { (observations, error) -> Void in if observations.count > 0 { let observation = observations[0] as AWFObservation self.currentWeatherIcon.setImageNamed(observation.icon) self.currentConditionsLabel.setText(observation.weather) self.currentLocationLabel.setText("\(city), \(state)") self.currentTemperatureLabel.setText("\(observation.tempF)°") let dateFormatter = NSDateFormatter() let formattedDate = NSDateFormatter.localizedStringFromDate(lastUpdatedAt, dateStyle: .ShortStyle, timeStyle: .ShortStyle) self.lastUpdatedLabel.setText(formattedDate) } else { println("No observations") if let e = error { println("error: \(e)") } } }) } else { println("No last location") } } |
Running the Watch App
Running now you should see something like this in the logs:
1 2 3 |
2014-11-29 09:14:51.445 aerisweather WatchKit Extension[14329:845403] <aerisweather_WatchKit_Extension.InterfaceController: 0x79e38cd0> init Last location: San Francisco, CA, at 2014-11-30 02:31:04 +0000 2014-11-29 09:14:52.677 aerisweather WatchKit Extension[14329:845506] Unable to find image named "cloudyn.png" on Watch |
and our corresponding watch app will show:
Oops! We need to add some assets to our app! Our iOS application currently contains a folder named Images.xcassets
with all of our weather icons. We’ll leverage this and include them into our watch app. To do so right-click on Images.xcassets in the aerisweather folder and select Show in Finder.
Now select your Images.xcassets icon in the aerisweather Watch App folder (there are three folders named Images.xcassets in your project, make sure and select the right one here!) and then drag-and-drop the contents of the folder in the Finder into Xcode in the icon pane of your Images.xcassets
. To be clear on this one: our existing iOS application contains all of the icons we need, but we need them on our watch as well. Using Show in Finder we expose the contents of our Images.xcassets so we can drag-and-drop the resources into the Watch App’s Images.xcassets!
Run the watch app again and you should see something like:
Get the Code on BitBucket
BitBucket contains two versions of the application:
- The starter application which does not include the Apple Watch target. Download this file if you want to go through the above tutorial from start to finish.
- The Apple Watch application which includes both the iPhone and Apple Watch apps. Download this file if you want to see the completed Apple Watch application.
Warning! Neither version comes with the ApiKeys.plist
file which contains the Aeris SDK API keys. You have to create this file yourself and obtain your own API keys from HAMweather.com!
You can also obtain fork us on BitBucket, just go to the main project page.
Final Thoughts
It’s been fun putting together this tutorial on creating an Apple Watch app. If you install and run the aerisweather app on your iPhone it does take a toll on the battery over the course of the day using the significant location change monitoring feature of CoreLocation. I’m a bit disappointed in that, but have become somewhat accustomed to recharging the iPhone every day. It will be interesting what types of applications are available for Apple Watch on launch day and what iOS features they take advantage of!
Questions or Comments
I’ve been through the tutorial a number of times starting with the aptly named starter application. If you have any difficulty going through the tutorial please send a tweet to @iachievedit!