awakeFromNib,outlet和storyboards:文档错了吗?

时间:2013-07-01 08:53:11

标签: ios uikit uistoryboard iboutlet

根据NSObject UIKit Additions Reference,出口变量应该在调用时间awakeFromNib时设置(强调所有我的):

  

nib-loading基础结构将awakeFromNib消息发送到从nib归档重新创建的每个对象,但只有在归档中的所有对象都已加载并初始化之后。 当对象收到awakeFromNib消息时,保证已建立所有插座和操作连接。

     

...

     

重要说明:由于无法保证从归档中实例化对象的顺序,因此初始化方法不应将消息发送到层次结构中的其他对象。 可以从awakeFromNib方法中安全地发送到其他对象的消息。

     

通常,您需要为需要在设计时无法完成的其他设置的对象实现awakeFromNib。例如,您可以使用此方法自定义任何控件的默认配置,以匹配用户首选项或其他控件中的值。您也可以使用它将单个控件恢复到应用程序的某个先前状态。

然而,这与我的测试不符,至少使用Storyboard。以下测试的结果似乎与文档相矛盾:

  • 在Xcode中创建一个新的单一视图应用程序。
  • 将第二个ViewController拖到故事板上。
  • 为第一个ViewController提供一个按钮,并从该按钮创建一个模式segue,显示第二个ViewController。
  • 为第二个ViewController创建一个ViewController类文件。
  • 在故事板上的第二个ViewController上创建一个标签,并从中创建一个名为someLabel的插座到相应的ViewController类。
  • 将以下awakeFromNib实现添加到第二个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");
    }
}
  • 在模拟器中运行应用程序,然后单击按钮。

当我这样做时,我会观察到以下记录:

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

由于这种行为,当我需要我的ViewControllers有一些涉及其出口的初始化逻辑时,我需要使用像答案here中提出的那样的黑客才能使用网点。如果我正确理解文档,我被迫使用这个hack的事实是UIKit行为中的一个错误,我应该能够将该初始化放在{{1}中并且只需使用没有任何黑客的插座。

我在互联网上找不到任何其他关于这个问题的提及,这看起来很奇怪,因为这对我来说是一个至关重要的功能。我也从未使用过实际的nib文件,只使用了故事板,所以我对此缺少一些看法,关于这些内容的文档冗长而且很难,作为iOS的新手,我不相信我已经理解了正确。这是一个真正的UIKit错误,还是我以某种方式误解了文档 - 也许这种方法甚至不能与故事板一起使用?

2 个答案:

答案 0 :(得分:15)

简答

您的视图控制器及其视图层次结构是在运行时从单独的nib文件加载的。

您的视图控制器首先加载并在加载其nib时接收awakeFromNib,但其视图层次结构nib尚未加载,因此在awakeFromNib中您不应假设任何出口视图层次结构尚未设置。

您的视图控制器在加载了视图层次结构nib后会收到viewDidLoad,因此在viewDidLoad中您可以假设所有出口都已设置。

长答案

当Xcode构建您的应用程序时,它会编译您的故事板。结果是包(Finder视为文件的文件夹)包含Info.plist和一堆.nib文件。我的一个项目的例子:

:; 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

Info.plist将故事板中的场景名称映射到相应的笔尖:

:; 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
}

如果场景中有一个故事板ID,或者segue连接到该场景,或者它是初始场景,则该场景仅显示在此列表中。

Info.plist中的nib文件列表包含这些视图控制器的视图层次结构。每个nib文件都包含其场景的视图控制器和场景中的任何其他顶级对象,但不包含视图控制器的视图或其任何子视图。

单独的nib文件包含场景的视图层次结构。视图层次结构nib的名称派生自视图控制器的对象ID及其顶级视图。您可以在Xcode的“Identity Inspector”中查看故事板中任何对象的对象ID。例如,我的“AccountCollection”场景的视图控制器的ID为BYZ-38-t0r,其视图的ID为8bC-Xf-vdC,因此场景的视图层次结构位于文件BYZ-38-t0r-view-8bC-Xf-vdC.nib中。场景nib文件包含其视图层次结构nib文件的名称:

:; 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

如果场景没有视图层次结构,那么视图控制器只会有一个nib文件,视图层次结构没有单独的nib文件。例如,UIPageViewController场景在故事板中没有视图层次结构,因此没有与UIPageViewController-ufv-JN-y6U.nib对应的视图层次结构笔尖。

那么这与你的问题有什么关系?这就是:当你的应用程序从“故事板”加载一个场景时,它正在加载包含视图控制器的nib文件(和其他顶部级别的对象)。当nib加载器完成加载该nib文件时,它会将awakeFromNib发送到它刚刚加载的所有对象。这包括您的视图控制器,但包含您的视图,因为您的视图不在该nib文件中。

稍后,当您的视图控制器被要求输入其view属性时,它会加载包含其视图层次结构的nib文件。视图控制器将自身作为-[UINib instantiateWithOwner:options:]参数传递给owner。这就是nib加载器如何将视图层次结构中的对象连接到视图控制器的出口和操作。

当nib加载程序完成加载视图层次结构nib时,它会将awakeFromNib发送到它刚刚加载的所有对象。由于您的视图控制器其中一个对象,因此您的视图控制器此时会收到awakeFromNib消息。

instantiateWithOwner:options:返回时,视图控制器会向自身发送viewDidLoad消息。这是您对视图层次结构进行更改的机会。

答案 1 :(得分:4)

查看控制器等到访问其视图以实际创建其视图。由于该按钮位于视图控制器的视图中,因此它不会被实例化。