Using Delayed Messages in Cocoa
Any framework on any platform is going to have some rough edges. One issue you can run into with Cocoa is that the objects in your nib file may not all load in the order you want them to.Generic controller classes can use the -awakeFromNib method to do some setup at startup. For example, you might need to prepare an NSArrayController. Ideally, you'd have a class that looks something like this:
@interface Controller : NSObject
{
IBOutlet NSArrayController * myArrayController;
}
@end
@implementation Controller
- (void)awakeFromNib
{
[myArrayController rearrangeObjects];
}
@end
It's not always quite that simple, though. If the Controller object loads before the myArrrayController instance does, you'll have a nil pointer and the -rearrangeObjects message will just be ignored. Here's where delayed messages can help.
There are two steps. First, we need to break off the actual setup code into a separate method:
- (void)prepareControllers
{
[myArrayController rearrangeObjects];
}
Second, we need to use a special NSObject method to send this message into the future. We'll put this call inside awakeFromNib:
- (void)awakeFromNib
{
[self performSelector: @selector(prepareControllers)
withObject: nil
afterDelay: 1.0];
}
And that's it. It's not a perfect solution because you're taking a bit of a chance with the timing, but it's fairly common in Cocoa. If nothing else, you should know it's a tool available to you.
Using Delayed Messages in Cocoa
Posted Sep 18, 2006 — 21 comments below
Posted Sep 18, 2006 — 21 comments below
Justin — Sep 18, 06 1820
From the NSNibAwakening Protocol doc:
An awakeFromNib message is sent to each object loaded from the archive, but only if it can respond to the message, and only after all the objects in the archive have been loaded and initialized. When an object receives an awakeFromNib message, it is guaranteed to have all its outlet instance variables set.
Dominik Wagner — Sep 18, 06 1821
What can happen though is that each of your objects in the nib file have to do something in awakeFromNib and the order in which awakeFromNib gets called isn't determined. So in that case you could use the perform with selector method. Although I would suggest you to load the nib yourself in that case and orchestrate your further initialisation using a normal Controller Object.
Another good reason for using the performSelector:withObject:afterDelay: is keeping your app responsive. If you have some time consuming task that can be split in loops or something then interrupting yourself with a performSelector: and a minimal delay will keep the beach ball from happening and give the user a chance to interact. However - with all the multi-cores out there you would probably be better of with doing the calculations in a separate thread.
Justin — Sep 18, 06 1822
Jon Hess — Sep 19, 06 1823
The sample code doesn't finish initializing the objects until 1.0 second after load, at the bottom of the event loop. If anyone starts using your objects between then, you could be hosed.
In that 1 second, the user could close the window you just loaded.
A more correct solution would be to have one of your objects (probably file's owner) have connections to the rest, and have it, deterministically initialize them.
performSelector:afterDelay: may seem neat at first, until you realize all of the stuff that can happen between the performSelector:afterDelay: call and the actual selector invocation.
Scott Stevenson — Sep 19, 06 1824
Good or bad, it does show up quite a bit in Cocoa code. Even if you don't use it, it's good to know what the intention is. Just my opinion.
Scott Stevenson — Sep 19, 06 1825
Nope.
Grayson — Sep 19, 06 1828
-(void)awakeFromNib
{
[myArrayController addObserver:self forKeyPath:@"content" options:0 context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == myArrayController)
{
[myArrayController rearrangeObjects];
[myArrayController removeObserver:self forKeyPath:@"content"];
}
}
Matt Gallagher — Sep 19, 06 1829
Also from NSNibAwaking protocol:
Messages to other objects can be sent safely from within awakeFromNib—by which time it’s assured that all the objects are unarchived and initialized (though not necessarily awakened, of course).
If you genuinely need to use performSelector:withObject:afterDelay: then the cause of the problem is either something else, or you've encountered a serious bug in NIB loading.
Evan — Sep 20, 06 1832
Scott Stevenson — Sep 20, 06 1836
That may be what it says on paper, but I'm 90% sure I've seen IBOutlets be nil by the time the app controller / window controller has its -awakeFromNib called. Maybe that was pre-Tiger, though.
Frank Reiff — Sep 21, 06 1837
Whether or not the IBOutlet instance variables are set or not, I don't know (I assume they are) but the Cocoa Bindings controller layer setup is definitely not always properly finished when awakeFromNib is called.
My own projects have fairly complicated nib structures and awakeFromNib on my main controller gets called at least a dozen times during initialization. This may be because it is the File Owner of a number of nibs, but Scott's example definitely rings a bell with me.
I had always assumed that everything would be set up by the time the awakeFromNib message was received, but in practice this didn't happen. Now at least I know it should work like that.
My solution was to make the main controller the application delegate and to use the applicationDidFinishLaunching to do my own deterministic setup.
Waiting 1 second may work, but then again it may not always work and I guess that's not good enough.
I think the loading issue may be more to do with the sequence in which Cocoa bindings resolves its bindings; it is easy to imagine that resolving one binding may have a knock-on effect on another binding, say in a typical list / detail view.
Now is Cocoa Bindings intelligent enough to always set the values in a consistent order? How does it deal with potential infinite loops, e.g. setting binding A changes binding B which changes binding A which changes..?
Any information about exactly how Cocoa Bindings performs its own initializaition would be appreciated..
Jens Alfke — Oct 14, 06 2066
I'd be very surprised if that were true — it would be quite a serious bug. What you probably saw was the case that does happen, where one object's awakeFromNib method calls into another object that got loaded from the same nib, whose awakeFromNib hasn't run yet, and so which has instance variables that are still nil.
What makes these awake-ordering dependencies even nastier is that the order in which objects are awoken will change from one launch to the next. The nib code is traversing a dictionary, meaning that the ordering depends on the exact hash values of the objects, and most view/controller objects don't provide custom has codes, which means the hash is simply the object pointer ... which is very likely to be different every time the app launches. I have definitely been bitten by that.
(This, by the way, is a close relative of the C++ problem of ordering dependencies in static object constructors.)
Scott Stevenson — Oct 14, 06 2068
Could be.
Ronzilla — Nov 02, 06 2276
If you have a NSController in entity mode (using Core Data), the fetch does not happen immediately, but in the next event. So what you will always see in your awakeFromNib is
- (void)awakeFromNib {
if (controller == nil) {
NSLog(@"check your connections in IB or file a bug");
} else {
if ([[controller arrangedObjects] count] == 0) {
NSLog(@"arraycontroller count is 0, this is normal");
}
}
}
I am an advocate of Grayson's approach - add yourself as an observer of the arrangedObjects in awakeFromNib.
Scott Stevenson — Nov 02, 06 2279
Mea culpa.
Josh Benjamin — Nov 13, 06 2395
If you separate your view(s) into a separate nib from the window, you can run into a nil value for [view window] if you call it in the awakeFromNib of your view-nib's File's Owner
Jacob Wallstrm — Mar 19, 07 3740
Joo Sampaio — Aug 18, 07 4570
- (void) awakeFromNib { NSError *error; BOOL ok = [myArrayController fetchWithRequest:nil merge:NO error:&error]; if (ok) { NSLog(@"Items : %i",[[myArrayController arrangedObjects] count]); } else { NSLog(@"err.."); } }
Nic Heath — Sep 07, 08 6344
I am setting up links in Interface Builder so my IBOutlet *mySearchBar is connected to the search bar my NIB.
My code looks as such
if (mySearchBar == NULL) NSLog(@"empty pointer");
prints out this in the console when I run it on my iPhone.
2008-09-07 01:08:44.540 HamInfo[15538:20b] empty pointer
When I use the method described in the post this fixes the problem. I'm confused as to why everyone is saying that the NIB is fully initialized and connected when the reference in my controller class is not connected by the time the program executes awakeFromNib.
Nic Heath — Sep 07, 08 6350
Scott Stevenson — Sep 07, 08 6355
I don't think that method exists in AppKit, but it looks like you were talking about iPhone. In any case, I think I had some sort of bug in my code that caused the outlets to be nil. I think Ronzilla's answer above is the one to go by for Mac desktop apps.