24

According to the NSObject UIKit Additions Reference, outlet variables should be set by the time awakeFromNib is called (emphasis all mine):

The nib-loading infrastructure sends an awakeFromNib message to each object recreated from a nib archive, but 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 and action connections already established.

...

Important: Because the order in which objects are instantiated from an archive is not guaranteed, your initialization methods should not send messages to other objects in the hierarchy. Messages to other objects can be sent safely from within an awakeFromNib method.

Typically, you implement awakeFromNib for objects that require additional set up that cannot be done at design time. For example, you might use this method to customize the default configuration of any controls to match user preferences or the values in other controls. You might also use it to restore individual controls to some previous state of your application.

However, this does not match my tests, at least using Storyboards. The results of the following test seem to contradict the documentation:

  • Create a new Single View Application in Xcode.
  • Drag a second ViewController onto the storyboard.
  • Give the first ViewController a button, and create a modal segue from that button that displays the second ViewController.
  • Create a ViewController class file for the second ViewController.
  • Create a label on the second ViewController on the storyboard and create an outlet called someLabel from it to the corresponding ViewController class.
  • Add the following awakeFromNib implementation to the second ViewController:

.

- (void) awakeFromNib {
    [super awakeFromNib];
    if (self.someLabel == nil) {
        NSLog(@"someLabel property is nil");
    }
    else {
        NSLog(@"someLabel property is not nil");
    }
    
    if (_someLabel == nil) {
        NSLog(@"_someLabel is nil");
    }
    else {
        NSLog(@"_someLabel is not nil");
    }
}
  • Run the app in the simulator and click the button.

When I do this, I observe the following logged:

2013-07-01 09:24:35.755 test[498:c07] someLabel property is nil
2013-07-01 09:24:35.758 test[498:c07] _someLabel is nil

As a consequence of this behaviour, when I need my ViewControllers to have some initialisation logic that involves their outlets, I need to use a hack like the one proposed in the answer here in order to be able to use the outlets. If I'm understanding the documentation correctly, the fact that I'm forced to use this hack is a bug in the UIKit behaviour, and I ought to be able to put that initialisation in awakeFromNib and simply use the outlets without any hacks.

I can't find any other mention of this issue on the internet, though, which seems odd given what a fundamentally important bit of functionality this appears (to me) to be. I've also never used actual nib files, only storyboards, so I'm missing some perspective on this, and the documentation on this stuff is verbose and difficult enough that as a newbie to iOS I'm not confident that I've understood correctly. Is this a genuine UIKit bug, or have I misunderstood the documentation in some way - perhaps this method isn't even meant to be used in conjunction with storyboards?

Community
  • 1
  • 1
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • 3
    Got it. This applies to `nib` files indeed, but _not_ storyboards. – Alladinian Jul 01 '13 at 09:23
  • @Alladinian Does the documentation say this, or is this what you observe from testing? If it's in the docs, what do they say about when `awakeFromNib` gets called for ViewControllers loaded from Storyboards and what guarantees exist at the time that it's called, and where is this documentation located? If it's from testing, perhaps provide a little extra detail - as an answer if necessary? In any case, thanks for the info. – Mark Amery Jul 01 '13 at 10:40
  • 1
    Hi Mark. The documentation doesn't say anything about Storyboards (in `awakeFromNib`), all this information applies to nibs. I have also tested it. Now the only thing kept me from posting this as an answer is that I cannot find any relevant documentation that _explicitly_ makes a note on this behavior. – Alladinian Jul 01 '13 at 10:47
  • @Alladinian Yeah, it just seems strange that the behaviour of `awakeFromNib` with regards to storyboards isn't documented at all. All we have to go on is the reasonable guess that its behaviour will be the same as when actually loading from a nib, but that seems not to be the case in reality. It also seems strange that the method even gets called when loading from a storyboard given that the guarantee of outlets being set is, as I understand it, the entire point of the method existing, yet in the storyboard case that guarantee is violated. – Mark Amery Jul 01 '13 at 10:55
  • 1
    Exactly! This I believe is the real question that needs a valid answer. – Alladinian Jul 01 '13 at 11:05
  • Relevant: http://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/ViewLoadingandUnloading/ViewLoadingandUnloading.html#//apple_ref/doc/uid/TP40007457-CH10 – Mark Amery Jul 03 '13 at 12:15
  • I have the same bug, I think this is a bug in ios7! It worked always for my storyboards and works randomely in some VCs but not in all of them! – João Nunes Feb 06 '14 at 12:36
  • 8
    *"When an object receives an awakeFromNib message, it is guaranteed to have all its outlet and action connections already established."* is a pretty unequivocal statement for something that's simply not true. – Robert Atkins Jun 23 '14 at 12:22

2 Answers2

19

Short Answer

Your view controller and its view hierarchy are loaded from separate nib files at runtime.

Your view controller is loaded first and receives awakeFromNib when its nib is loaded, but its view hierarchy nib hasn't been loaded yet, so in awakeFromNib you shouldn't assume any outlets to the view hierarchy have been set yet.

Your view controller receives viewDidLoad after its view hierarchy nib has been loaded, so in viewDidLoad you can assume all outlets have been set.

Long Answer

When Xcode builds your app, it compiles your storyboard. The result is a package (a folder that Finder treats as a file) containing an Info.plist and a bunch of .nib files. Example from one of my projects:

:; pwd
/Users/mayoff/Library/<snip>/Pinner.app/Base.lproj/Main.storyboardc
:; ll
total 80
drwxr-xr-x  10 mayoff  staff   340 May 11 22:13 ./
drwxr-xr-x   4 mayoff  staff   136 May 11 22:13 ../
-rw-r--r--   1 mayoff  staff  1700 May 11 22:13 AccountCollection.nib
-rw-r--r--   1 mayoff  staff  1110 May 11 22:13 AccountEditor.nib
-rw-r--r--   1 mayoff  staff  2999 May 11 22:13 BYZ-38-t0r-view-8bC-Xf-vdC.nib
-rw-r--r--   1 mayoff  staff   439 May 11 22:13 Info.plist
-rw-r--r--   1 mayoff  staff  7621 May 11 22:13 LqH-9K-CeF-view-OwT-Ts-HoG.nib
-rw-r--r--   1 mayoff  staff  6570 May 11 22:13 OZq-QF-pn5-view-xSR-gK-reL.nib
-rw-r--r--   1 mayoff  staff  2473 May 11 22:13 UINavigationController-ZKB-z3-xgf.nib
-rw-r--r--   1 mayoff  staff   847 May 11 22:13 UIPageViewController-ufv-JN-y6U.nib

The Info.plist maps the scene names in your storyboard to the corresponding nibs:

:; plutil -p Info.plist 
{
  "UIViewControllerIdentifiersToNibNames" => {
    "AccountCollection" => "AccountCollection"
    "UINavigationController-ZKB-z3-xgf" => "UINavigationController-ZKB-z3-xgf"
    "UIPageViewController-ufv-JN-y6U" => "UIPageViewController-ufv-JN-y6U"
    "AccountEditor" => "AccountEditor"
  }
  "UIStoryboardDesignatedEntryPointIdentifier" => "UINavigationController-ZKB-z3-xgf"
  "UIStoryboardVersion" => 1
}

A scene only shows up in this list if it has a storyboard ID, or a segue connects to it, or it is the initial scene.

The nib files list in Info.plist do not contain the view hierarchies of those view controllers. Each of those nib files contains the view controller of its scene and any other top-level objects in the scene, but not the view controller's view or any of its subviews.

A separate nib file contains the view hierarchy for the scene. The name of view hierarchy nib derived from the object IDs of the view controller and its top-level view. You can see the object ID of any object in your storyboard in the “Identity Inspector” in Xcode. For example, my “AccountCollection” scene's view controller's ID is BYZ-38-t0r and its view's ID is 8bC-Xf-vdC, so the view hierarchy for the scene is in file BYZ-38-t0r-view-8bC-Xf-vdC.nib. The scene nib file contains the name of its view hierarchy nib file:

:; strings - AccountCollection.nib |grep -e '-.*-'
UIPageViewController-ufv-JN-y6U
BYZ-38-t0r-view-8bC-Xf-vdC          <---------
UpstreamPlaceholder-5Hn-fK-fqQ
UpstreamPlaceholder-8GL-mk-Rao
q1g-aL-SLo.title

If a scene doesn't have a view hierarchy, then there will just be a nib file for the view controller and no separate nib file for the view hierarchy. For example, a UIPageViewController scene doesn't have a view hierarchy in the storyboard so there's no view hierarchy nib corresponding to UIPageViewController-ufv-JN-y6U.nib.

So what does all this have to do with your question? Here's what: when your app loads a scene from the “storyboard”, it's loading the nib file containing the view controller (and other top-level objects). When the nib loader finishes loading that nib file, it sends awakeFromNib to all the objects it just loaded. This includes your view controller, but it does not include your views, because your views weren't in that nib file.

Later, when your view controller is asked for its view property, it loads the nib file containing its view hierarchy. The view controller passes itself to -[UINib instantiateWithOwner:options:] as the owner argument. This is how the nib loader can connect objects in the view hierarchy to the view controller's outlets and actions.

When the nib loader finishes loading the view hierarchy nib, it sends awakeFromNib to all the objects it just loaded. Since your view controller was not one of those objects, your view controller does not receive an awakeFromNib message at this time.

When instantiateWithOwner:options: returns, the view controller sends itself the viewDidLoad message. This is your opportunity to make changes to the view hierarchy.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
4

View controllers wait until their view is accessed to actually create their view. Since the button is in the view controller's view, it won't be instantiated yet.

nevan king
  • 112,709
  • 45
  • 203
  • 241
  • 3
    Sure, I know that this seems to be the behaviour in practice - hence the standard hack of accessing the `.view` property of a ViewController before doing view initialisation logic. But the question here is whether that's *supposed* to be the case, or whether there's a bug or misdocumentation in the framework somewhere. As far as I can tell from the docs, I *should* be able to access outlet views on a `ViewController` without first accessing its `.view` property, and *that's* what this question is about. – Mark Amery Jul 01 '13 at 09:21