You're viewing the legacy docs. They are deprecated as of May 18, 2016.
These docs are for version 2.5.1 and below of the Objective-C SDK. Go to our current docs, or see our iOS migration guide.

Version 2.5.1Changelog

Using WatchKit with Firebase

It's easy to add Firebase to your WatchKit app. This guide will give you an overview of WatchKit and walk you through the steps of integrating Firebase into your Watch app.

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>
The following documentation refers to watchOS 1.0.

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.

If the Apple Watch simulator doesn't load go to the menu bar for the iOS simulator and select Hardware > External Displays > Apple Watch 38mm/42mm.

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.