Best practices for the iOS UIViewController and Firebase

The UIViewController comes with a lifecycle that informs us when important events occur. Events such as viewDidLoad,viewWillAppear, viewDidDisappear, and the always fun Stop! You’re using too much memory! warning.
The UIViewController’s lifecycle is a huge convenience. But it can be confusing to know when to do what. This article will cover a few best practices to help you develop with confidence.
Since we’re developers, we’ll use a zero-based index for this list.

Best Practices

0. Initialize references in viewDidLoad

override func viewDidLoad() {
  super.viewDidLoad()
  ref = Firebase(url: "https://<YOUR-FIREBASE-APP>.firebaseio.com")
}
- (void)viewDidLoad {
  [super viewDidLoad];
  self.ref = [[Firebase alloc] initWithUrl:@"https://<YOUR-FIREBASE-APP>.firebaseio.com"];
}

You can’t count on a UIViewController’s initializer. This is because controllers that come from the Storyboard don’t have their initializer called. This leaves us with the viewDidLoad method.
The viewDidLoad method is usually the first method we care about in the UIViewController lifecycle. Since viewDidLoad is called once and only once in the lifecycle, it’s a great place for initialization.
The viewDidLoad method is also called whether you use Storyboards or not. Outlets and properties are set at this point as well. This will enable you to do any dynamic creation of a reference’s location.

1. Initialize references with implicitly unwrapped optionals (Swift-only)

class ViewController : UIViewController {
  var ref: Firebase!

  override func viewDidLoad() {
    super.viewDidLoad()
    ref = Firebase(url: "https://<YOUR-FIREBASE-APP>.firebaseio.com")
  }
}

In Swift all properties’ values must be set before initialization is complete. And that’s a big problem. You can’t rely on a UIViewController’s initializer to ever be called. So how do you set the value for the Firebase reference property? Use an implicitly unwrapped optional.

By unwrapping the property the compiler will assume the value will exist by the time it’s called. If the value is nil when called, it’ll crash the app. That won’t happen for a reference property if the value is set in viewDidLoad.

Using an implicitly unwrapped optional is you telling the compiler: “Chill-out, I know what I’m doing.”
You might be wondering why you shouldn’t inline the value.

class ViewController : UIViewController {
  let ref = Firebase(url: "https://<YOUR-FIREBASE-APP>.firebaseio.com")
}

There’s no problem using an inline value. You’re just limited to static values since you can’t use other properties or variables.

class ViewController : UIViewController {
  // This won't compile :(
  let ref = Firebase(url: "https://my.firebaseio.com/(myCoolProperty)")
}

2. Create listeners in viewWillAppear, not in viewDidLoad

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)
  ref.observeEventType(.Value) { (snap: FDataSnapshot!) in print (snap.value) }
}
- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
  [self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
    NSLog(@"%@", snapshot.value);
  }];
}

Your app should be a good citizen of battery life and memory. To preserve battery life and memory usage, you should only synchronize data when the view is visible.

The viewWillAppear method is called each time the view becomes visible. This means if you set your listener here, your data will always be in sync when the view is visible.

You should avoid creating listeners in viewDidLoad. Remember that viewDidLoad only gets called once. When the view disappears you should remove the listener. This means the data won’t re-sync when the view becomes visible again.

3. Remove listeners in viewDidDisappear with a FirebaseHandle

class ViewController : UIViewController {
  var ref: Firebase!
  var handle: UInt!

  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    handle = ref.observeEventType(.Value) { (snap: FDataSnapshot) in print (snap.value) }
  }
}
@interface ViewController()
@property (nonatomic, strong) Firebase *ref;
@property FirebaseHandle handle;
@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.ref = [[Firebase alloc] initWithUrl:@"https://<YOUR-FIREBASE-APP>.firebaseio.com"];
}

- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
  self.handle = [self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
    NSLog(@"%@", snapshot.value);
  }];
}

@end

To remove a listener in iOS you need a FirebaseHandle. A FirebaseHandle is just a typealias for a UInt that keeps track of a Firebase listener.

Note that in Swift you need to use an implicitly unwrapped optional since the value can’t be set in the initializer. The handle’s value is set from the return value of the listener.
Use this handle to remove the listener in viewDidDisappear.

override func viewDidDisappear(animated: Bool) {
  super.viewDidDisappear(animated)
  ref.removeObserverWithHandle(handle)
}
-(void)viewDidDisappear:(BOOL)animated {
  [super viewDidDisappear:animated];
  [self.ref removeObserverWithHandle:self.handle];
}

If your controller is still syncing data when the view has disappeared, you are wasting bandwidth and memory.

Leaky Listeners

A leaky listener is a listener that is consuming memory to store data that isn’t displayed or accessed. This is especially an issue when navigating using a UINavigationController, since the root controller isn’t removed from memory when navigating to a detail controller. This means a root controller will continue to synchronize data if the listener isn’t removed when navigating away. This action takes up needless bandwidth and memory.

The thought of removing the listener might sound unappealing. You may think you need to keep your listener open to avoid downloading the same data again, but this is unnecessary.
Firebase SDKs come baked in with caching and offline data persistence. These features keep the client from having to fetch recently downloaded data.

4. Enable offline in the AppDelegate’s initializer

class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  override init() {
    Firebase.defaultConfig().persistenceEnabled = true
  }
}
@implementation AppDelegate

- (instancetype)init
{
  self = [super init];
  if (self) {
    [[Firebase defaultConfig] setPersistenceEnabled:YES];
  }
  return self;
}

@end

Speaking of offline, this tip isn’t UIViewController specific, but it’s important. Offline has to be set before any other piece of Firebase code runs.

Your first instinct might be to enable offline in the AppDelegate’s application:didFinishLaunchingWithOptions method. This will work for most situations, but not for all. This can go wrong when you inline a Firebase property’s value in the root UIViewController. The value of the Firebase property is set before application:didFinishLaunchingWithOptions gets called, which will cause the SDK to throw an exception.

By setting up the offline config in the AppDelegate init we can avoid this issue.

Final example

Check out this gist to see the final version of the UIViewController.

Takeaways

If you remember anything remember these things:

  • Initialize references in viewDidLoad
  • Synchronize data only when the view is visible
  • Store a handle to simplify removing a reference
  • Remove listeners when the view in not visible
  • Configure offline persistence in the AppDelegate’s initializer

How do you manage Firebase in your UIViewController? Let us know if you’re using any of these today or if you have any best practices of your own.