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.
Design Element
Using Delayed Messages in Cocoa
Posted Sep 18, 2006 — 21 comments below




 

Justin — Sep 18, 06 1820

Interesting. From reading the docs, I thought that it was guaranteed that every object in the NIB file is loaded by the time awakeFromNib is called. Though I have read of this problem elsewhere as well.

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

Yeah - the above stated case can't happen.

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

To get around needing awakeFromNib: called in a certain order, you could either move some initialization to initWithCoder: or initWithFrame: (as long as it doesn't depend on outlets being connected), or implement an ordering system, maybe with every object registers with the app controller from awakeFromNib and then is initialized once every object has registered.

Jon Hess — Sep 19, 06 1823

I cringe whenever I see -[NSObject performSelector:withObject:afterDelay:] In code. Especially when delay is non-zero.

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 Scotty the Leopard

performSelector:afterDelay: may seem neat at first, until you realize all of the stuff that can happen

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 Scotty the Leopard

I thought that it was guaranteed that every object in the NIB file is loaded by the time awakeFromNib is called

Nope.

Grayson — Sep 19, 06 1828

I've never had a problem with the connection being nil. I have noticed that the content isn't already there so `rearrangedObjects` wouldn't work. If the connection actually is nil, then waiting may be the best thing to do but I've always observed changes to the "content" value of an array controller. So far, it's worked for me. I also get to work with it as soon as the content is available and I don't have to worry about timing issues.

-(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

I agree with Justin, awakeFromNib is always called after all objects in the NIB are fully loaded and initialised.

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

The array controller won't be nil, it just hasn't performed a fetch yet so -arrangedObjects is empty.

Scott Stevenson — Sep 20, 06 1836 Scotty the Leopard

I agree with Justin, awakeFromNib is always called after all objects in the NIB are fully loaded and initialised

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

I'm afraid I'm with Scott on this one.

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

"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"

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 Scotty the Leopard

whose awakeFromNib hasn't run yet, and so which has instance variables that are still nil

Could be.

Ronzilla — Nov 02, 06 2276

Your outlets WILL NOT be nil during awakeFromNib. If you see this, file a bug.
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 Scotty the Leopard

Your outlets WILL NOT be nil during awakeFromNib

Mea culpa.

Josh Benjamin — Nov 13, 06 2395

Your outlets may not be nil, but it gets interesting if you have an application that has a view which can be put in a pane or its own separate window.

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

I'm experiencing this problem and found this. I would agree with Dominik (comment 1821) that what is happening is that an object is getting awakeFromNib before all other objects have gotten it. I'm using bindings.

Joo Sampaio — Aug 18, 07 4570

You can always force it to be fetched.. this solves my problem.

- (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 ran into this problem tonight and here is something interesting.
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

The solution is to use viewDidLoad method. It seems awakeFromNib message is not guaranteed to be sent at the correct time.

Scott Stevenson — Sep 07, 08 6355 Scotty the Leopard

@Nic Heath: The solution is to use viewDidLoad method

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.




 

Comments Temporarily Disabled

I had to temporarily disable comments due to spam. I'll re-enable them soon.





Copyright © Scott Stevenson 2004-2015