加载UIView子类的Nib的正确方法

101

我知道这个问题以前已经被问过了,但答案是矛盾的,我感到困惑,所以请不要抨击我。

我想在整个应用程序中拥有可重用的UIView子类。我想使用一个nib文件来描述接口。

现在假设它是一个带有活动指示器的加载指示器视图。我想在某些事件上实例化此视图并将其动画化到视图控制器的视图中。我可以很容易地通过编程方式描述视图的接口,创建元素并在init方法中设置它们的框架等。

但如何使用nib呢?保持界面构建器中给定的大小而无需设置框架。

我已经设法像这样做了,但我确定这是错误的(它只是一个带有选择器的视图):

 - (id)initWithDataSource:(NSDictionary *)dataSource {
        self = [super init];
        if (self){
            self = [[[NSBundle mainBundle] loadNibNamed:[NSString stringWithFormat:@"%@", [self class]] owner:self options:nil] objectAtIndex:0];
            self.pickerViewData = dataSource;
            [self configurePickerView];
        }
        return self;
    }

但是我正在覆盖自己,当我实例化它时:

FSASelectView *selectView = [[FSASelectView alloc] initWithDataSource:selectViewDictionary];
    selectView.delegate = self;

    selectView.frame = CGRectMake(0, self.view.bottom + 50, [FSASelectView width], [FSASelectView height]);

我需要手动设置框架而不是从IB中获取。

编辑:我想在视图控制器中创建此自定义视图,并可以访问控制视图的元素。我不想要一个新的视图控制器。

谢谢

编辑:我不知道这是否是最佳实践,我确定不是,但这是我的做法:

FSASelectView *selectView = [[[NSBundle mainBundle] loadNibNamed:[NSString stringWithFormat:@"%@",[FSASelectView class]] owner:self options:nil] objectAtIndex:0];
    selectView.delegate = self;
    [selectView configurePickerViewWithData:ds];
    selectView.frame = CGRectMake(0, self.view.bottom + 50, selectView.width, selectView.height);
    selectView.alpha = 0.9;
    [self.view addSubview:selectView];
    [UIView animateWithDuration: 0.25 delay: 0 options:UIViewAnimationOptionAllowUserInteraction |UIViewAnimationOptionCurveEaseInOut animations:^{
                            selectView.frame = CGRectMake(0, self.view.bottom - selectView.height, selectView.width, selectView.height);
                            selectView.alpha = 1;
                        } completion:^(BOOL finished) {
                        }];

正确的做法仍然需要

这个操作应该使用视图控制器和nib名称进行初始化吗?我应该在代码中的某个UIView初始化方法中设置nib吗?还是我所做的方式可以接受?


但是第一行代码几乎与您在“initWithDataSource”中使用的原始问题中的代码相同,不过无论如何,即使您将所有者设置为'Nil',这也会起作用。 - Rakesh
值得注意的是,如今99.99999%的情况下,我们都会使用容器视图,这在iOS中已经被广泛使用。操作指南文章 - Fattie
请注意,您可以将 [NSString stringWithFormat:@"%@",[FSASelectView class]] 替换为 NSStringFromClass([FSASelectView class])。 - naomimichiko
6个回答

132
MyViewClass *myViewObject = [[[NSBundle mainBundle] loadNibNamed:@"MyViewClassNib" owner:self options:nil] objectAtIndex:0]

我正在使用这个来初始化我所拥有的可重用自定义视图。


请注意,您可以在结尾处使用"firstObject",它更简洁。 "firstObject"是NSArray和NSMutableArray的一个方便方法。

这里是一个典型的例子,加载xib以用作表头。 在您的文件YourClass.m中:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    return [[NSBundle mainBundle] loadNibNamed:@"TopArea" owner:self options:nil].firstObject;
}

通常,在 TopArea.xib 中,您会单击“文件所有者”并将文件所有者设置为您的类。 然后,在 YourClass.h 中实际上会有 IBOutlet 属性。 在 TopArea.xib 中,您可以将控件拖到这些 outlet 上。

不要忘记在 TopArea.xib 中,如果需要,您可能需要单击“视图本身”,并将其拖动到某个输出口,以便您可以对其进行控制。(一个非常有价值的提示是,当您为表格单元格行执行此操作时,您绝对必须这样做 - 您必须将视图本身连接到代码中相关的属性。)


UIView 没有 initWithNibName: 方法,这个方法只适用于 UIViewController。 - meronix
那是为视图控制器设计的,我想使用一个UIView,并让创建它的控制器拥有它。 - Adam Waite
我会将其标记为正确。我不知道它是否是最佳实践,但它有效。 - Adam Waite
@Joe Blow 对于打扰,我很抱歉,但是为什么要这样做呢:“不要忘记,在TopArea.xib中,您可能需要单击视图本身并将其拖动到某个插座,以便在必要时对其进行控制。” 你能否直接使用myViewObject来访问它作为UIView的基础视图?为什么需要属性? - user1686342
嗯...我不知道 :) 我忘了...已经有好几年没人再使用nib视图了。去用容器视图吧!https://dev59.com/KGAg5IYBdhLWcg3wm7_6#23403979 正如你所建议的,我在写作时可能混淆了视图控制器和视图。感谢指正... - Fattie
显示剩余2条评论

39

如果你想让你的CustomView和它的xib文件独立于File's Owner,请按照以下步骤进行:

  • File's Owner字段留空。
  • 在你的CustomViewxib文件中点击实际视图,并将其Custom Class设置为CustomView(即你的自定义视图类的名称)
  • 在你的自定义视图的.h文件中添加IBOutlet
  • 在你的自定义视图的.xib文件中,点击视图并进入Connection Inspector。在这里,你将看到你在.h文件中定义的所有IBOutlets
  • 将它们分别连接到它们相应的视图上。

在你的CustomView类的.m文件中,重写init方法如下:

-(CustomView *) init{
    CustomView *result = nil;
    NSArray* elements = [[NSBundle mainBundle] loadNibNamed: NSStringFromClass([self class]) owner:self options: nil];
    for (id anObject in elements)
    {
        if ([anObject isKindOfClass:[self class]])
        {
            result = anObject;
            break;
        }
    }
    return result;
}

现在,当您想要加载您的CustomView时,请使用以下代码:[[CustomView alloc] init];


1
截至iOS 9,这是本页面上唯一对我有效的解决方案(其他解决方案会导致崩溃)。 - lifjoy
请注意,这种方法不适用于从现有XIB加载自定义视图,因为它不会调用“-init”。相反,它将调用“initWithFrame:”和“-awakeFromNib”。如果您编写与“-init”方法中加载视图相同的代码,则会进入无限循环。 - Pierre

25

请按照以下步骤进行操作:

  1. 创建名为 MyView.h/.m 的 UIView 类。
  2. 创建同名的 xib 文件 MyView.xib
  3. 在 xib 中将“文件拥有者”类从 NSObject 更改为 UIViewController。参见下面的图片 enter image description here
  4. 将“文件拥有者”的视图连接到您的视图。参见下面的图片 enter image description here

  5. 将您的视图的类更改为 MyView,与第 3 步相同。

  6. 放置控件并创建 IBOutlets。

以下是加载视图的代码:

UIViewController *controller=[[UIViewController alloc] initWithNibName:@"MyView" bundle:nil];
MyView* view=(MyView*)controller.view;
[self.view addSubview:myview];

希望能够帮助。

澄清:

UIViewController用于加载你的xib文件,而UIViewController所拥有的视图实际上是在MyView xib中分配的MyView视图。

演示 我制作了一个演示,在这里可以获取。


为什么有人对这个进行了负投票,是错的吗?虽然我还没有尝试过,但是看起来这似乎不能做到我想要的。 - Adam Waite
我曾经给你的回答点了个踩,但那是因为我误读了你的回答(以为你把一个UIView子类分配为文件的所有者)。对此我感到很抱歉。 - Rakesh
2
在你的xib文件中忽略文件所有者。然后,你可以通过执行MyView *view = [[UINib instantiateWithOwner:nil options:nil] lastObject];来避免创建一个UIViewController。 - LightningStryk
1
@LightningStryk 当然可以,但这只是另一种方法。 - iphonic
1
这是为了创建一个可重用的UIView子类,而不是使用UIViewController来实现该目的。 - arielyz
@arielyz 注意仔细看答案,这是另外一种做法。顺便说一下,我没有使用UIViewController而是访问了与其附加的视图。 - iphonic

11

回答自己的问题,大约两年后...

它使用了协议扩展,因此您可以对所有类执行此操作,无需任何额外的代码。

/*

Prerequisites
-------------
- In IB set the view's class to the type hook up any IBOutlets
- In IB ensure the file's owner is blank

*/

public protocol CreatedFromNib {
    static func createFromNib() -> Self?
    static func nibName() -> String?
}

extension UIView: CreatedFromNib { }

public extension CreatedFromNib where Self: UIView {

    public static func createFromNib() -> Self? {
        guard let nibName = nibName() else { return nil }
        guard let view = NSBundle.mainBundle().loadNibNamed(nibName, owner: nil, options: nil).last as? Self else { return nil }
        return view
    }

    public static func nibName() -> String? {
        guard let n = NSStringFromClass(Self.self).componentsSeparatedByString(".").last else { return nil }
        return n
    }
}

// Usage:
let myView = MyView().createFromNib()

8

在Swift中:

例如,您的自定义类名为 InfoView

首先,您需要创建以下文件: InfoView.xib InfoView.swift

import Foundation
import UIKit

class InfoView: UIView {
    class func instanceFromNib() -> UIView {
    return UINib(nibName: "InfoView", bundle: nil).instantiateWithOwner(nil, options: nil)[0] as! UIView
}

然后将File's Owner设置为UIViewController,如下所示:

enter image description here

将您的View重命名为InfoView

enter image description here

右键单击File's Owner,将您的view字段与InfoView连接起来:

enter image description here

确保类名为InfoView

enter image description here

在此之后,您可以在自定义类中轻松地向按钮添加操作:

enter image description here

并在您的MainViewController中使用此自定义类:
func someMethod() {
    var v = InfoView.instanceFromNib()
    v.frame = self.view.bounds
    self.view.addSubview(v)
}

2

你可以使用视图控制器初始化xib并使用viewController.view,或者按照你所做的方式进行操作。仅将UIView子类作为UIView的控制器是一个不好的想法。

如果您的自定义视图没有任何输出口,则可以直接使用UIViewController类来初始化它。

更新: 在您的情况下:

UIViewController *genericViewCon = [[UIViewController alloc] initWithNibName:@"CustomView"];
//Assuming you have a reference for the activity indicator in your custom view class
CustomView *myView = (CustomView *)genericViewCon.view;
[parentView addSubview:myView];
//And when necessary
[myView.activityIndicator startAnimating]; //or stop

否则,您必须创建一个自定义的UIViewController(将其作为文件的所有者,以便正确连接插座)。
YourCustomController *yCustCon = [[YourCustomController alloc] initWithNibName:@"YourXibName"].

您可以在任何您想要添加视图的地方使用它。

[parentView addSubview:yCustCon.view];

然而,当你在加载xib时,将另一个视图控制器(已用于另一个视图)作为所有者传递并不是一个好主意,因为控制器的视图属性会被改变,当你想访问原始视图时,你将没有对它的引用。
编辑:如果你已经使用相同的主UIViewController类设置了新的xib,并将视图属性绑定到新的xib视图,那么你将面临这个问题。
即:
- YourMainViewController——管理——mainView - CustomView——需要根据需要从xib加载。
下面的代码会在以后造成困惑,如果你将其编写到YourMainViewController的viewDidLoad中。这是因为从这一点开始,self.view将指向你的customview。
-(void)viewDidLoad:(){
  UIView *childView= [[[NSBundle mainBundle] loadNibNamed:@"YourXibName" owner:self options:nil] objectAtIndex:0];
}

不一定。但是,从单独的xib加载视图会是一个无问题的方式。IOS5之前的苹果文档说一个视图控制器不应该用于控制多个场景(xib),反之亦然。 - Rakesh
我个人认为,是否需要创建一个新的控制器取决于代码/xib的复杂程度。如果你能成功地处理所引起的问题,并且不考虑未来的修改,那就没有问题。但是创建一个单独的视图控制器会为您解决很多问题。而且,由于在您的情况下只是一个活动指示器,您可以轻松地使用UIViewController(如答案中所述)对象。我会相应地更新我的答案。 - Rakesh
我认为那不对。创建的视图控制器是多余的。你只使用了它的视图属性。 - Adam Waite
1
我可以在界面构建器中绘制界面,而不是通过编程来实现。虽然通过编程实现也不是问题,但我想知道如何对UIView进行子类化并为其提供一个nib文件!这应该不难。 - Adam Waite
让我们在聊天中继续这个讨论。点击此处进入聊天室 - Rakesh
显示剩余4条评论

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