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.