如何使用由Interface Builder创建的nib文件加载UIView

244

我正在尝试做一些有点复杂但应该可行的事情。所以这里向所有专家们发出挑战(这个论坛上有很多你们 : ) )。

我正在创建一个“问卷”组件,我想在NavigationContoller上加载它(我的QuestionManagerViewController)。这个“组件”是一个“空”的UIViewController,根据需要回答的问题可以加载不同的视图。

我正在这样做:

  1. 创建Question1View对象作为UIView子类,并定义一些IBOutlets
  2. 创建(使用界面构建器)Question1View.xib(这可能是我的问题所在)。我将UIViewControllerUIView都设置为Question1View类。
  3. 我使用IB将输出链接到视图的组件。
  4. 我重写了我的QuestionManagerViewControllerinitWithNib,使它看起来像这样:

    - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
        if (self = [super initWithNibName:@"Question1View" bundle:nibBundleOrNil]) {
            // Custom initialization
        }
        return self;
    }
    
    当我运行代码时,出现了以下错误信息:

    2009-05-14 15:05:37.152 iMobiDines[17148:20b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[UIViewController _loadViewFromNibNamed:bundle:] loaded the "Question1View" nib but the view outlet was not set.'


    我确信有一种方法可以使用nib文件加载视图,而不需要创建viewController类。
24个回答

224

除了将 nib 视为数组处理之外,还有一种更简单的方法来访问视图。

1)创建一个自定义的 View 子类,并添加要稍后访问的任何 outlet。--MyView

2)在你想要加载和处理 nib 的 UIViewController 中,创建一个 IBOutlet 属性,用于保存加载的 nib 视图,例如:

在 MyViewController(一个 UIViewController 子类)中:

  @property (nonatomic, retain) IBOutlet UIView *myViewFromNib;

(别忘了在.m文件中将其综合并释放)

3) 在IB中打开你的nib(我们称之为'myViewNib.xib'),将文件所有者设置为 MyViewController

4) 现在将文件所有者outlet myViewFromNib连接到nib中的主视图。

5) 现在在MyViewController中,写下以下代码:

[[NSBundle mainBundle] loadNibNamed:@"myViewNib" owner:self options:nil];

现在一旦你这样做了,调用你的属性"self.myViewFromNib"将使你可以从你的nib文件中获取到视图!


4
它会适用于主控制器视图(self.view)吗?由于某些原因,我需要在标准控制器初始化方法后从nib文件中加载主视图。原因是复杂的子类结构。 - Lukasz
@Lukasz 你成功做了多少?我也在寻找像你一样的东西。 - Ajay Sharma
抱歉,我可能有点笨,但是我对第一点感到困惑。如何在自定义视图子类中创建插座?我以为插座只适用于UIViewControllers? - jowie
1
如果这个视图出现在多个父视图上怎么办?你是建议手动编辑每一个xib文件吗?太糟糕了! - user2083364
2
这仅适用于您想在单个视图控制器中使用自定义nib的情况。如果您想在不同的视图控制器上使用它,每个都动态创建自定义nib的实例,则无法正常工作。 - thgc
有没有办法在Storyboard中添加视图时使用它?有人成功过吗?这是一种优雅的变体,如果能与Storyboard一起使用会更加优雅。谢谢。 - invoodoo

121

谢谢大家。 我找到了实现我想要的功能的方法。

  1. 使用所需的IBOutlet创建您的UIView
  2. 在IB中创建xib,按照您的喜好进行设计,并像这样链接它:文件所有者属于UIViewController类(没有自定义子类,而是“真实”的类)。文件所有者的视图连接到主视图,其类声明为步骤1)中的类。
  3. 将您的控件与IBOutlet连接。
  4. DynamicViewController可以运行其逻辑以决定要加载的视图/xib。一旦做出决定,在loadView方法中放置类似以下内容:

    NSArray* nibViews = [[NSBundle mainBundle] loadNibNamed:@"QPickOneView"
                                                      owner:self
                                                    options:nil];
    
    QPickOneView* myView = [ nibViews objectAtIndex: 1];
    
    myView.question = question;
    

就是这样!

主捆绑包的loadNibNamed方法将负责初始化视图并创建连接。

现在,视图控制器可以根据内存中的数据显示一个视图或另一个视图,而“父”屏幕不需要关心这个逻辑。


2
我有一个非常类似的问题 - 我只想从xib文件中加载一个UIView。这些步骤对我不起作用 - 在将加载的视图添加为子视图后不久,应用程序会崩溃并显示“bad access”。 - Justicle
2
对于iPhone 3.0,使用objectAtIndex:0来获取第一个元素。这对我来说正如Justicle所描述的那样崩溃了。有什么想法为什么会这样? - tba
1
+1,它对我来说完美地运行了。我想添加多个视图,这些视图具有一个视图控制器(我正在使用翻转视图场景,其中视图的一部分旋转)。我必须将数组的索引设置为0(iPhone 3.0)。 - Aran Mulholland
43
这是正确的答案,但代码需要修改,以便循环遍历nibViews并对每个对象进行类检查,这样您才能确保获取正确的对象。objectAtIndex:1是一种危险的假设。 - M. Ryan
2
Jasconius是正确的。你应该像这样循环遍历nibViews数组:https://gist.github.com/769539 - leviathan
显示剩余4条评论

72

我不确定一些答案在说什么,但是我需要在下次在Google搜索时将这个答案放在这里。关键词: “如何从一个nib文件中加载UIView” 或者 “如何从NSBundle中加载UIView”。

这里的代码几乎100%来自Apress出版的Beginning iPhone 3书本(第247页,“使用新的TableView Cell”):

- (void)viewDidLoad {
    [super viewDidLoad];
    NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:@"Blah"
                                                 owner:self options:nil];
    Blah *blah;
    for (id object in bundle) {
        if ([object isKindOfClass:[Blah class]]) {
            blah = (Blah *)object;
            break;
        }
    }   
    assert(blah != nil && "blah can't be nil");
    [self.view addSubview: blah];
} 

假设你有一个名为BlahUIView子类,一个名为Blah的nib文件,其中包含一个UIView,其类设置为Blah


Category: NSObject+LoadFromNib

#import "NSObject+LoadFromNib.h"

@implementation NSObject (LoadFromNib)

+ (id)loadFromNib:(NSString *)name classToLoad:(Class)classToLoad {
    NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil];
    for (id object in bundle) {
        if ([object isKindOfClass:classToLoad]) {
            return object;
        }
    }
    return nil;
}

@end

Swift扩展

extension UIView {
    class func loadFromNib<T>(withName nibName: String) -> T? {
        let nib  = UINib.init(nibName: nibName, bundle: nil)
        let nibObjects = nib.instantiate(withOwner: nil, options: nil)
        for object in nibObjects {
            if let result = object as? T {
                return result
            }
        }
        return nil
    }
}

以下是使用示例:

class SomeView: UIView {
    class func loadFromNib() -> SomeView? {
        return self.loadFromNib(withName: "SomeView")
    }
}

2
如果您使用isKindOf,则可以保护自己免受Bundle结构更改的影响。 - Dan Rosenstark
1
一个 assert 可能是更好的解决方案。这样你只会隐藏问题。 - Sulthan
1
@Sulthan,assert不同于其他的内容。我想要类型为Blah 的对象无论位于Nib中的哪个位置*。我不在乎它是否被提升或降级。然而,针对您的评论,我已添加了一个assert,但它并不代替for循环。它是对其的补充。 - Dan Rosenstark
你可以使用没有参数的loadFromNib方法:
  • (id)loadFromNib{ NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil]; for (id object in bundle) { if ([object isKindOfClass:[self class]]) { return object; } } return nil; }
- JVC

22
对于那些需要管理多个自定义视图实例的人,即“Outlet Collection”,我将@Gonso、@AVeryDev和@Olie的答案合并并进行了定制化处理: 1. 创建自定义的MyView:UIView,并将其设置为所需XIB中根UIView的“Custom Class”; 2. 在MyView中创建所有需要的outlets(现在就这样做,因为在第3步之后,IB会建议您将outlets连接到UIViewController,而不是我们想要的自定义视图); 3. 将您的UIViewController设置为自定义视图XIB的“File's Owner”; 4. 在UIViewController中为每个MyView实例添加一个新的UIViews,并将它们连接到UIViewController创建一个Outlet Collection:这些视图将充当自定义视图实例的“包装器”视图; 5. 最后,在viewDidLoad中添加以下代码行:
NSArray *bundleObjects;
MyView *currView;
NSMutableArray *myViews = [NSMutableArray arrayWithCapacity:myWrapperViews.count];
for (UIView *currWrapperView in myWrapperViews) {
    bundleObjects = [[NSBundle mainBundle] loadNibNamed:@"MyView" owner:self options:nil];
    for (id object in bundleObjects) {
        if ([object isKindOfClass:[MyView class]]){
            currView = (MyView *)object;
            break;
        }
    }

    [currView.myLabel setText:@"myText"];
    [currView.myButton setTitle:@"myTitle" forState:UIControlStateNormal];
    //...

    [currWrapperView addSubview:currView];
    [myViews addObject:currView];
}
//self.myViews = myViews; if need to access them later..

你知道如何通过编程实现这个功能吗?而不是使用xib文件吗? - The X-Coder

19

我会使用UINib来实例化一个自定义的UIView以便重用

UINib *customNib = [UINib nibWithNibName:@"MyCustomView" bundle:nil];
MyCustomViewClass *customView = [[customNib instantiateWithOwner:self options:nil] objectAtIndex:0];
[self.view addSubview:customView];

在这种情况下需要的文件是 MyCustomView.xibMyCustomViewClass.hMyCustomViewClass.m。 请注意,[UINib instantiateWithOwner] 返回一个数组,因此您应该使用反映您想要重新使用的 UIView 的元素。在这种情况下,它是第一个元素。


1
第二行不应该是"rankingNib",而应该是"customNib",你这么认为对吗? - Danny

11

不要在Interface Builder中将您的视图控制器的类设置为UIView的子类。这肯定是问题的一部分,将其保留为UIViewController、它的某个子类或其他自定义类。

至于仅从xib中加载视图,我假设如果您想使用xib来定义界面,则必须将某种类型的视图控制器(即使它不扩展UIViewController,对于您的需求来说可能太重)设置为File's Owner。我进行了一些研究以确认这一点。这是因为否则将无法访问UIView中的任何接口元素,也不会触发您自己的代码中的事件。

如果您将UIViewController作为您的视图控制器的File's Owner,您只需使用initWithNibName:bundle:来加载视图,并获取视图控制器对象。在IB中,请确保将view输出连接到xib中包含界面的视图。如果您使用其他类型的对象作为File's Owner,则需要使用NSBundleloadNibNamed:owner:options:方法来加载nib,向该方法传递File's Owner的实例。所有的属性都会根据您在IB中定义的输出正确设置。


我正在阅读Apress出版的书籍《Beginning iPhone Development: Exploring the iPhone SDK》,他们在第9章“导航控制器和表视图”中讨论了这种方法。它看起来并不太复杂。 - Lyndsey Ferguson
我很好奇他们是如何说这件事的。目前我没有那本书的副本。 - Marc W

9
您也可以使用UIViewController的initWithNibName方法而不是loadNibNamed。我觉得这样更简单易懂。
UIViewController *aViewController = [[UIViewController alloc] initWithNibName:@"MySubView" bundle:nil];
[self.subview addSubview:aViewController.view];
[aViewController release];  // release the VC
现在,您只需要创建MySubView.xib和MySubView.h/m文件。在MySubView.xib中,将File's Owner类设置为UIViewController,将视图类设置为MySubView。 您可以使用父xib文件来定位和调整子视图的大小。

8

这是一件本应更容易的事情。我最终扩展了 UIViewController 并添加了一个 loadNib:inPlaceholder: 选择器。现在我可以说

self.mySubview = (MyView *)[self loadNib:@"MyView" inPlaceholder:mySubview];
这是类别的代码(它执行与Gonso描述的相同的程序):

@interface UIViewController (nibSubviews)

- (UIView *)viewFromNib:(NSString *)nibName;
- (UIView *)loadNib:(NSString *)nibName inPlaceholder:(UIView *)placeholder;

@end

@implementation UIViewController (nibSubviews)

- (UIView *)viewFromNib:(NSString *)nibName
{
  NSArray *xib = [[NSBundle mainBundle] loadNibNamed:nibName owner:self options:nil]; 
  for (id view in xib) { // have to iterate; index varies
    if ([view isKindOfClass:[UIView class]]) return view;
  }
  return nil;
}

- (UIView *)loadNib:(NSString *)nibName inPlaceholder:(UIView *)placeholder
{
  UIView *nibView = [self viewFromNib:nibName];
  [nibView setFrame:placeholder.frame];
  [self.view insertSubview:nibView aboveSubview:placeholder];
  [placeholder removeFromSuperview];
  return nibView;
}

@end

8
这是一个很好的问题(+1),答案几乎都有帮助 ;) 很抱歉,但我花了很长时间才找到答案,尽管Gonso和AVeryDev都给出了很好的提示。希望这个答案能帮助其他人。 MyVC是持有所有这些内容的视图控制器。 MySubview是我们想要从xib中加载的视图。
  • In MyVC.xib, create a view of type MySubView that is the right size & shape & positioned where you want it.
  • In MyVC.h, have

    IBOutlet MySubview *mySubView
    // ...
    @property (nonatomic, retain) MySubview *mySubview;
    
  • In MyVC.m, @synthesize mySubView; and don't forget to release it in dealloc.

  • In MySubview.h, have an outlet/property for UIView *view (may be unnecessary, but worked for me.) Synthesize & release it in .m
  • In MySubview.xib
    • set file owner type to MySubview, and link the view property to your view.
    • Lay out all the bits & connect to the IBOutlet's as desired
  • Back in MyVC.m, have

    NSArray *xibviews = [[NSBundle mainBundle] loadNibNamed: @"MySubview" owner: mySubview options: NULL];
    MySubview *msView = [xibviews objectAtIndex: 0];
    msView.frame = mySubview.frame;
    UIView *oldView = mySubview;
    // Too simple: [self.view insertSubview: msView aboveSubview: mySubview];
    [[mySubview superview] insertSubview: msView aboveSubview: mySubview]; // allows nesting
    self.mySubview = msView;
    [oldCBView removeFromSuperview];
    
对我来说棘手的部分是:其他答案中的提示从xib中加载了我的视图,但没有替换MyVC中的视图(呃!) - 我必须自己交换它。 另外,要访问mySubview的方法,必须将.xib文件中的view属性设置为MySubview。否则,它将返回为普通的UIView。 如果有一种直接从其自己的xib加载mySubview的方法,那就太好了,但这让我达到了我需要到达的地方。

1
我使用了这个方法,谢谢。为了支持嵌套视图,我做了一个改变,将 [self.view insertSubview: msView aboveSubview: mySubview]; 改为 [mySubview.superview insertSubview: msView aboveSubview: mySubview]; - Jason

7

在Swift中

实际上,我解决这个问题的方法是,在我想要使用该视图的CustomViewController的viewDidLoad中加载视图,如下所示:

myAccessoryView = NSBundle.mainBundle().loadNibNamed("MyAccessoryView", owner: self, options: nil)[0] as! MyAccessoryView
不要在loadView()方法中加载视图! loadView方法用于为自定义视图控制器加载视图。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,