Creating a New Watch App
This guide assumes you have XCode with WatchKit installed. If you're new to iOS development you can install the latest version of XCode from the Mac App Store.
Open XCode and create a new project. Select iOS > Application > Single View Application. Once the project is created click File > New > Target > Apple Watch. Inside the Apple Watch screen select WatchKit App, click Finish, and then activate the scheme when prompted.
Creating a WatchKit App Extension
CocoaPods Setup
Once the main project and WatchKit App Extension is created we'll need to install Firebase through CocoaPods.
Installing CocoaPods
CocoaPods is a dependency manager for iOS and OSX development. To install CocoaPods run the following command:
$ sudo gem install cocoapods
We'll need to close the current XCode project and open up a terminal to the location of the project. The pod init
command will create a boilerplate Podfile
at the root of the project.
$ pod init
Open the Podfile
and remove the boilerplate code. Declare Firebase as a dependency of the host app and link the dependency to the WatchKit Extension with the link_with
command. Be sure to replace <your-project-name> with the name of your Xcode project.
pod 'Firebase', '>= 2.5.1'
link_with '<your-project-name>', '<your-project-name> WatchKit Extension'
Run the install command. Make sure you're inside of the root directory where the Podfile
is located.
$ pod install
CocoaPods will create a workspace that contains our project and another project containing its dependencies. Once the install is completed we can open up our new workspace in XCode. Make sure to open up the .xcworkspace
file and not the .xcproject
file.
$ open <your-project-name>.xcworkspace
For Swift users, the last step to set up Firebase is to create a bridging header in our Host app and WatchKit App Extension targets. In both targets create a new Objective-C file and call it temp. XCode will prompt to create a bridging header, select yes. Inside of the bridging header file XCode created import Firebase. Delete the temp Objective-C file.
#import <Firebase/Firebase.h>
Watch App Architecture
There are three main pieces of a Watch App: the Host app, the WatchKit Extension, and the WatchApp.
Host app
The Host app is the main app of the project. This is the app the user launches from the iOS device.
WatchKit Extension
The WatchKit Extension is an iOS App Extension that runs of the iOS device. As of watchOS 1.0, the code for a Watch App runs on an iOS device and not the Apple Watch itself.
WatchApp
The WatchApp project contains the interface for the Watch App. This is the only part of the Watch App that runs on the Apple Watch.
Creating the Interface
Let's build a simple interface . Open the Interface.storyboard
in the WatchKit App target. Watch the clip below to see how to add a label and button to your watch view.
Remove any notifications screens if they appear on the storyboard.
In our app we'll need to access the label and know when the button is touched. We'll wire up an outlet
for the label and an action
for the button. One way of acheiving this is to open up the Interface.storyboard
and select the InterfaceController
. Open up the Assistant Editor to show the InterfaceController
code file to the right. From there we can control click on to set up our outlets
and actions
To create the outlet
for the label, select the label and control drag to the InterfaceController
to the right. In the popup give it a name, we'll call it labelUpdate
, and make sure Outlet
is selected.
To create the action
for the button touch, select the button and control drag to the InterfaceController
to the right. In the popup give it a name, we'll call it updateButtonIsTouched
, and make sure Action
is selected.
Add your outlet and action to the top of your InterfaceController
class. They should look something like this:
@IBOutlet weak var awesomeLabel: WKInterfaceLabel!
@IBAction func updateButtonIsTouched() {
}
@interface InterfaceController()
@property (weak, nonatomic) IBOutlet WKInterfaceLabel *labelUpdate;
@end
@implementation InterfaceController
- (IBAction)updateButtonDidTouch {
}
@end
See the clip for a demo of wiring up your label and buttons to your controller.
Creating a Firebase reference
In the WatchKit Extension, open up the InterfaceController
. Add a Firebase
reference property.
class InterfaceController: WKInterfaceController {
// reference property
var ref: Firebase!
// Outlets
@IBOutlet weak var awesomeLabel: WKInterfaceLabel!
}
#import "InterfaceController.h"
#import <Firebase/Firebase.h>
@interface InterfaceController()
// reference property
@property (strong, nonatomic) Firebase *ref;
// Outlets
@property (weak, nonatomic) IBOutlet WKInterfaceLabel *labelUpdate;
@end
The WKInterfaceController
lifecycle has a set of methods where we can initialize a reference, sync data, and remove syncing events. Using the awakeWithContext
function, we can initialize the reference created above.
WKInterfaceController Lifecycle
Here we'll describe how to add Firebase to your app in each stage of the lifecycle. A sequence of functions are called as a WKInterfaceController
progresses through its lifecycle.
awakeWithContext
The awakeWithContext
function is called when the WKInterfaceController
has loaded. Because this function is called only when the controller is loaded, it is a great place to initialize Firebase references.
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
ref = Firebase(url: "https://<your-firebase-app>.firebaseio.com/updates")
}
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
self.ref = [[Firebase alloc] initWithUrl:@"https://<your-firebase-app>.firebaseio.com/updates"];
}
willActivate
The willActivate
function is called when the view appears on screen. Since this function is called when the view is brought back, it's perfect for setting up observers to a Firebase database.
override func willActivate() {
super.willActivate()
ref.observeEventType(.ChildAdded, withBlock: { (snapshot: FDataSnapshot!) in
println(snapshot.value)
})
}
- (void)willActivate {
[super willActivate];
[self.ref observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
NSLog(@"%@", snapshot.value);
}];
}
didDeactivate
The didDeactivate
function is called when the view goes off screen. This is where we can remove any observers on our Firebase reference property.
override func didDeactivate() {
super.didDeactivate()
ref.removeAllObservers()
}
- (void)didDeactivate {
[super didDeactivate];
[self.ref removeAllObservers];
}
Saving Data
Using the reference in the interface controller we'll save a new timestamp everytime the user taps the button.
@IBAction func updateDidTouch() {
ref.childByAutoId().setValue(FirebaseServerValue.timestamp())
}
- (IBAction)updateButtonDidTouch {
[[self.ref childByAutoId] setValue: [FirebaseServerValue timestamp]];
}
Sycing Data
Using our ref
property we can sync data in the willActivate
function.
override func willActivate() {
super.willActivate()
ref.observeEventType(.ChildAdded, withBlock: { (snap: FDataSnapshot!) -> Void in
if snap.exists() {
self.labelUpdate.setText(snap.value as? String)
} else {
self.labelUpdate.setText("No update")
}
})
}
- (void)willActivate {
[super willActivate];
[self.ref observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
if ([snapshot exists]) {
[self.labelUpdate setText:[snapshot.value stringValue]];
} else {
[self.labelUpdate setText:@"No update"];
}
}];
}
To make sure we're not syncing data while off-screen, we'll need to remove any observers in the didDeactivate
function.
override func didDeactivate() {
super.didDeactivate()
ref.removeAllObservers()
}
- (void)didDeactivate {
[super didDeactivate];
[self.ref removeAllObservers];
}
Running the Simulator
To run a WatchKit app in the simulator, set the active scheme to the WatchKit app and hit run. The iPhone/iPad simulator should load with a second simulator for the Apple Watch.
After the simulator has loaded, tap the update button. Our label will update with the timestamp we sent out to Firebase.
Creating a WKTableInterface
WatchKit provides a table control called WKTableInterface
. This control is similar to UITableView
in UIKit. Let's make our app display timestamps as they're added to Firebase. To do this we'll delete our existing label, add a WKInterfaceTable
above the current button, and drag a label into the table. See the clip below for a demo:
Using the Interface.storyboard
drag a table on to the view. Each table is made up of rows. Each row type can be assigned an identifier to programatically create instances of the row. For this table's row we'll simply call it "TableRow".
Each table row needs a class to serve as its model. We'll define our model in a new TableRow
class in <your_app_name> WatchKit Extension/TableRow.swift
. This model is just a plain class that extends from NSObject
. Inside of the model we can add any appropriate outlets
and actions
. By default XCode will import the Foundation library for us. We'll need to import the WatchKit
library below it. The following clip demonstrates defining a table class and wiring it up to your view.
//In the WatchKit Extension target
import Foundation
import WatchKit
class TableRow : NSObject {
@IBOutlet weak var labelUpdate: WKInterfaceLabel!
}
// TableRow.h
#import <Foundation/Foundation.h>
#import <WatchKit/WatchKit.h>
@interface TableRow : NSObject
@end
// TableRow.m
#import "TableRow.h"
@interface TableRow()
@property (weak, nonatomic) IBOutlet WKInterfaceLabel *labelUpdate;
@end
@implementation TableRow
@end
The model class must be associated with the row in the Interface.storyboard
. Select the table row and then select the identity inspector. Here we can select our backing model class, such as the TableRow
class above.
To access the table interface on our view, create an outlet in the InterfaceController
.
// inside the InterfaceController class
@IBOutlet weak var table: WKInterfaceTable!
// inside the InterfaceController.m interface
@property (weak, nonatomic) IBOutlet WKInterfaceTable *table;
Binding to a WKTableInterface
Now that the table has a row class and an outlet in the view we can bind data to each row. To bind data to a WKTableInterface
we'll use .ChildAdded
, .ChildRemoved
, and .ChildChanged
events to re-render the table from remote updates. We'll keep track of the items added in a property array of FDataSnapshot
.
In the properties section of the controller add a an array of FDataSnapshot
property.
var ref: Firebase!
var updates: [FDataSnapshot]!
@property (strong, nonatomic) Firebase *ref;
@property (strong, nonatomic) NSMutableArray *updates;
In the awakeWithContext
function initialize the array.
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
ref = Firebase(url: "https://<your-firebase-app>.firebaseio.com/updates")
updates = [FDataSnapshot]()
}
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
self.ref = [[Firebase alloc] initWithUrl:@"https://<your-firebase-app>.firebaseio.com/updates"];
// Initialize the array
self.updates = [[NSMutableArray alloc] init];
}
In the willActivate
function add the following set of listeners:
.ChildAdded
Add an .ChildAdded
observer which will receive the most recently added snapshot of the list. We'll bind the newest item to a row in the table when the observer fires off.
// Listen for children added to add new rows to the table
ref.observeEventType(.ChildAdded, withBlock: { [unowned self] (snapshot: FDataSnapshot!) -> Void in
// Add to the local array of snapshots
self.updates.append(snapshot)
// Create the index for the NSIndexSet
var index = self.updates.count - 1
// Insert the row into the table
self.table.insertRowsAtIndexes(NSIndexSet(index: index), withRowType: "TableRow")
// Set up the row
if let row = self.table.rowControllerAtIndex(index) as? TableRow {
row.labelUpdate.setText(snapshot.value.description)
}
})
// Listen for children added to add new rows to the table
[self.ref observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
// Add to the local array of snapshots
[self.updates addObject:snapshot];
// Create the index for the NSIndexSet
NSUInteger index = self.updates.count - 1;
NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndex:index];
// Insert the row into the table
[self.table insertRowsAtIndexes:indexSet withRowType:@"TableRow"];
// Set up the row, check for string and number in snapshot.value
TableRow *row = [self.table rowControllerAtIndex:index];
if ([snapshot.value isKindOfClass:[NSNumber class]]) {
NSNumber *numberSnap = snapshot.value;
[row.labelUpdate setText:[numberSnap stringValue]];
} else if ([snapshot.value isKindOfClass:[NSString class]]){
NSString *stringSnap = snapshot.value;
[row.labelUpdate setText:stringSnap];
}
}];
.ChildRemoved
Add an .ChildRemoved
observer which will receive the most recently removed snapshot of the list. We'll remove the item from the table by finding the item in the local array. To do this we'll need to create a little helper function in the InterfaceController
.
Create a helper function called findIndexOfSnapshotFromArrayByKey
:
// Find a snapshot by its key
func findIndexOfSnapshotFromArrayByKey(array: [FDataSnapshot!], key: String) -> Int? {
for (index, item) in enumerate(array) {
let snapshot = item as FDataSnapshot;
if snapshot.key == key {
return index
}
}
return nil;
}
// Find a snapshot by its key
- (int)findIndexOfSnapshotFromArrayByKey:(NSMutableArray *)array :(NSString *) key {
for (int i=0; i < array.count; i++) {
id item = array[i];
if ([item isKindOfClass:[FDataSnapshot class]]) {
FDataSnapshot *snapshot = item;
if ([snapshot.key isEqualToString: key]) {
return i;
}
}
}
return -1;
}
Inside of willActivate
add the following observer:
// Listen for children removed to remove rows from the table
ref.observeEventType(.ChildRemoved, withBlock: { [unowned self] (snapshot: FDataSnapshot!) in
// Find the index of the item being removed in the local array
if let indexToRemove = self.findIndexOfSnapshotFromArrayByKey(self.updates, keysnapshot.key) {
// Remove from the local array and from the table
self.updates.removeAtIndex(indexToRemove)
self.table.removeRowsAtIndexes(NSIndexSet(index: indexToRemove))
}
})
// Listen for children removed to remove rows from the table
[self.ref observeEventType:FEventTypeChildRemoved withBlock:^(FDataSnapshot *snapshot) {
// Find the index of the item being removed in the local array
int index = [self findIndexOfSnapshotFromArrayByKey:self.updates : snapshot.key];
// Create the index for the NSIndexSet
NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndex:index];
// Remove from the local array and from the table
[self.updates removeObjectAtIndex:index];
[self.table removeRowsAtIndexes:indexSet];
}];
When items are removed from our Firebase database the table will remove its associated row.
.ChildChanged
Add an .ChildChanged
observer which will receive the most recently changed snapshot of the list.
// Listen for children whose values have changed and re-render the row
ref.observeEventType(.ChildChanged, withBlock: { [unowned self] (snapshot: FDataSnapshot!) in
// Find the index of the item that has changed
if let indexToChange = self.findIndexOfSnapshotFromArrayByKey(self.updates, key: snapshot.key) {
// Replace the old snapshot with the new one
self.updates[indexToChange] = snapshot
// Remove the old row
self.table.removeRowsAtIndexes(NSIndexSet(index: indexToChange))
// Insert the new row
self.table.insertRowsAtIndexes(NSIndexSet(index: indexToChange), withRowType: "TableRow")
// Set up the row
if let row = self.table.rowControllerAtIndex(indexToChange) as? TableRow {
row.labelUpdate.setText(snapshot.value.description)
}
}
})
// Listen for children whose values have changed and re-render the row
[self.ref observeEventType:FEventTypeChildChanged withBlock:^(FDataSnapshot *snapshot) {
// Find the index of the item that has changed
int indexToChange = [self findIndexOfSnapshotFromArrayByKey:self.updates : snapshot.key];
// Create the index for the NSIndexSet
NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndex:indexToChange];
// Replace the old snapshot with the new one
self.updates[indexToChange] = snapshot;
// Remove the old row
[self.table removeRowsAtIndexes:indexSet];
// Insert the new row
[self.table insertRowsAtIndexes:indexSet withRowType:@"TableRow"];
// Set up the row, check for string and number in snapshot.value
TableRow *row = [self.table rowControllerAtIndex:indexToChange];
if ([snapshot.value isKindOfClass:[NSNumber class]]) {
NSNumber *numberSnap = snapshot.value;
[row.labelUpdate setText:[numberSnap stringValue]];
} else if ([snapshot.value isKindOfClass:[NSString class]]){
NSString *stringSnap = snapshot.value;
[row.labelUpdate setText:stringSnap];
}
}];
Inside the observer we're finding the changed item through the findIndexOfSnapshotFromArrayByKey
helper function. From there we can find the index of the item in the updates
property. With the index we can now replace the old snapshot with the new one. Then we insert the new row, delete the old one, and set up the TableRow
for the new item.
Now whenever we add, update, move, or remove any items in from this location in the Firebase database our list will re-render appropriately.
User Authentication
iOS App Extensions, like Watch Apps, are in a separate bundle from the host app. There are a few gotchas with authentication since the user cannot enter their credentials to log in from the app.
One way of authenticating a user within an iOS App Extension is to use App Groups to share data across different targets. Using App Groups we can store a user's authentication token in an NSUserDefaults
session in the host app. Once the authentication token has been stored the App Extension can retrieve it and call authWithCustomToken
.
Enabling App Groups
To enable App Groups, select the project in the Navigator. Select the host app target, and then select Capabilities. Within the Capabilities section turn App Groups to "On". The default group can be used, but a custom one can be created as well. The group name should look something like: group.username.SuiteName
. Now select the WatchApp target. Repeat the same process above, and make sure to enable the same App Group, group.username.SuiteName
. See the clip below for enabling App Groups in your project.
Saving the authentication token
Inside of the host app, store the user's Firebase auth token after they have authenticated. This is normally done in a UIViewController
that handles login.
let defaults = NSUserDefaults(suiteName: "group.username.SuiteName")!
ref.observeAuthEventWithBlock { [unowned self] (authData: FAuthData!) in
if authData != nil {
defaults.setObject(authData.token, forKey: "FAuthDataToken")
defaults.synchronize()
}
}
Firebase *ref = [[Firebase alloc] initWithUrl:@"https://<your-firebase-app>.firebaseio.com/"];
NSUserDefaults *defaults = [[NSUserDefaults alloc]initWithSuiteName:@"group.username.SuiteName"];
[ref observeAuthEventWithBlock:^(FAuthData *authData) {
if (authData) {
[defaults setObject:authData.token forKey:@"FAuthDataToken"];
[defaults synchronize];
}
}];
Use an NSUserDefaults
object to store the authentication token. When the authentication state has changed to authenticated, store the authData.token
value in the NSUerDefaults
object. Make sure to call synchronize
or the data will not be persisted.
Authenticating WatchKit Extension Users
Now that the authentication token is stored, the WatchKit Extension can access it from an NSUserDefaults
object as well.
Add the following code snippet to the awakeWithContext
method in the InterfaceController
:
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
ref = Firebase(url: "https://<your-firebase-app>.firebaseio.com/updates")
updates = [FDataSnapshot]()
// Use the same suiteName as used in the host app
let defaults = NSUserDefaults(suiteName: "group.username.SuiteName")!
// Grab the auth token
let authToken = defaults.objectForKey("FAuthDataToken") as? String
// Authenticate with the token from the NSUserDefaults object
ref.authWithCustomToken(authToken, withCompletionBlock: { [unowned self] (error: NSError!, authData: FAuthData!) in
if authData != nil {
println("Authenticated inside of the Watch App!")
} else {
println("Not authenticated")
}
})
}
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
self.ref = [[Firebase alloc] initWithUrl:@"https://<your-firebase-app>.firebaseio.com/updates"];
self.updates = [[NSMutableArray alloc] init];
// Use the same suiteName as used in the host app
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.username.SuiteName"];
// Grab the auth token
NSString *authToken = [defaults objectForKey:@"FAuthDataToken"];
// Authenticate with the token from the NSUserDefaults object
[self.ref authWithCustomToken:authToken withCompletionBlock:^(NSError *error, FAuthData *authData) {
if (authData != nil) {
NSLog(@"Authenticated inside of the Watch App!");
} else {
NSLog(@"Not authenticated");
}
}];
}
Once the token has been retrieved from the NSUserDefaults
object, call authWithCustomToken
function with the authentication token.
Handling Unauthenticated WatchKit Extension Users
If the user is unauthenticated they'll just see a blank screen. The withCompletionBlock
will not return an authData
parameter if the authentication was unsuccessful.
Modify the withCompletionBlock
to the following code:
ref.authWithCustomToken(authToken, withCompletionBlock: { [unowned self] (error: NSError!, authData:FAuthData!) in
if authData != nil {
println("Authenticated inside of the Watch App!")
} else {
// Create a dummy row
self.table.insertRowsAtIndexes(NSIndexSet(index: 0), withRowType: "TableRow")
if let row = self.table.rowControllerAtIndex(0) as? TableRow {
// Give it a message informing the user to log in
row.labelUpdate.setText("Please log in")
}
}
})
// Authenticate with the token from the NSUserDefaults object
[self.ref authWithCustomToken:authToken withCompletionBlock:^(NSError *error, FAuthData*authData) {
if (authData != nil) {
NSLog(@"Authenticated inside of the Watch App!");
} else {
// Create a dummy row
NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndex:0];
[self.table insertRowsAtIndexes:indexSet withRowType:@"TableRow"];
// Give it a message informing the user to log in
id row = [self.table rowControllerAtIndex:0];
if ([row isKindOfClass:[TableRow class]]) {
TableRow *tableRow = row;
[tableRow.labelUpdate setText:@"Please log in"];
}
}
}];
If authData
is nil
, a label or table row can inform the user that they are not logged in. In this example we're creating a dummy row that asks the user to log in.
Now our users should be able to log in from the host app and be authenticated within the watch app. If you have any questions reach out to us in our community forum.