EGGER APPS

The Egger Apps Blog

01 Feb 2014

Cocoa: An observer block that removes itself

NSNotificationCenter has a very convenient method for registering a block as an observer. However, getting that code right is very tricky. Let's say we open a window and want some code to run when the window closes. An obvious way would be to use some code like the following:

[[NSNotificationCenter defaultCenter] addObserverForName: NSWindowWillCloseNotification
                                                  object: aWindow 
                                                   queue: nil 
                                              usingBlock: ^(NSNotification *note) {
    NSLog(@"Window closed.");
}];

The problem with the above code is that we never clean up and remove the observer, which will leak memory. Assuming that we want to run this code exactly once, we might come up with the following idea to remove the observer:

id observer = [[NSNotificationCenter defaultCenter] addObserverForName: NSWindowWillCloseNotification
                                                                object: aWindow 
                                                                 queue: nil 
                                                            usingBlock: ^(NSNotification *note) {
    NSLog(@"Window closed.");
    [[NSNotificationCenter defaultCenter] removeObserver:observer];
}];

Now that wont work, because the block captures the value of a variable at the time it is created. But the block is created before observer is set. Therefore the value of observer inside the block is undefined!

The logical conclusion of course is to make the variable a __block variable. Then, when the variable is set, the change will be visible inside the block.

__block id observer = [[NSNotificationCenter defaultCenter] addObserverForName: NSWindowWillCloseNotification
                                                                        object: aWindow 
                                                                         queue: nil 
                                                                    usingBlock: ^(NSNotification *note) {
    NSLog(@"Window closed.");
    [[NSNotificationCenter defaultCenter] removeObserver:observer];
}];

Are we done yet? No. We just ended up with a retain cycle. The problem is that the block retains the observer, and the the observer retains the block. To break this cycle, we must set observer to nil:

__block id observer = [[NSNotificationCenter defaultCenter] addObserverForName: NSWindowWillCloseNotification
                                                                        object: aWindow 
                                                                         queue: nil 
                                                                    usingBlock: ^(NSNotification *note) {
    NSLog(@"Window closed.");
    [[NSNotificationCenter defaultCenter] removeObserver:observer];
    observer = nil;
}];

When the variable is set to nil, the observer will be released, and it will in turn release the block. Problem solved!

This example shows yet again that you can't blindly trust ARC. Especially when working with blocks, you should always think twice about who retains what.