具有自己XIB的UIView子类

21

我创建了一个自定义的UIView子类,并且不想在UIView子类中使用代码来布局UI,我想使用xib。因此,我做了以下操作。

我创建了一个名为“ShareView”的类,它是UIView的子类。我创建了一个XIB文件,将其文件所有者设置为“ShareView”。然后,我链接了在我的“ShareView.h”中声明的一些输出口。

接下来,我有一个ViewController,MainViewController,它将ShareView添加为子视图。使用以下代码:

NSArray *arr = [[NSBundle mainBundle] loadNibNamed:@"ShareView" owner:nil options:nil];
UIView *fv = [[arr objectAtIndex:0] retain];
fv.frame = CGRectMake(0, 0, 320, 407);
[self.view addSubview:fv];

现在我在我声明的ShareView中遇到了NSUnknownKeyException错误。

之所以这样做是因为我想要一个UIView,在它自己的独立XIB文件中具有自己的逻辑。我在几个地方阅读到,ViewController仅用于管理全屏幕,即不是屏幕的部分...那么我做错了什么?我希望将ShareView的逻辑放在一个单独的类中,这样我的MainController类不会因为ShareView的逻辑而变得臃肿(我认为这是解决此问题的一种选择)


2
我发现https://dev59.com/wm445IYBdhLWcg3wE2Je#5056886是解决这个问题的最佳方案。 - Willster
6个回答

26

ThomasM,

我们有关于将行为封装在自定义视图内(比如,一个带有最小值/最大值/当前值标签和处理值变化事件的滑块控件)的相似想法。

在我们目前的最佳实践中,我们会在Interface Builder中设计ShareView(ShareView.xib),就像Eimantas在他的回答中所描述的那样。然后,我们将ShareView嵌入到MainViewController.xib的视图层次结构中。

我在我们的iOS开发者博客中写了一篇文章,介绍了如何嵌入自定义视图Nib到其他Nib中。关键是在你的自定义视图中重写-awakeAfterUsingCoder:方法,用从“嵌入”的Nib(ShareView.xib)中加载的对象替换从MainViewController.xib中加载的对象。

大致如下:

// ShareView.m
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder {
    BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);
    if (theThingThatGotLoadedWasJustAPlaceholder) {
        // load the embedded view from its Nib
        ShareView* theRealThing = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([ShareView class]) owner:nil options:nil] objectAtIndex:0];

        // pass properties through
        theRealThing.frame = self.frame;
        theRealThing.autoresizingMask = self.autoresizingMask;

        [self release];
        self = [theRealThing retain];
    }
    return self;
}

1
听起来很不错,但是当使用 ARC 时,你无法在 init 方法外部分配给 self :-( - Ben Clayton
4
关于 ARC,你说得对。请参见我的后续帖子,网址为http://blog.yangmeyer.de/blog/2012/07/09/an-update-on-nested-nib-loading/。 - Yang Meyer
使用这种技术需要注意的是,theRealThing 的 awakeFromNib 方法会被调用两次(即同一个实例)。如果你的实现覆盖了该方法,需要考虑到这一点。 - DTs
1
@Yang Meyer,您回答中的链接以及博客文章已经失效了。能否请您更新一下? - Nick Weaver

6
您将加载的xib文件的所有者定义为nil。由于xib文件本身的文件所有者已连接出口并定义为ShareView的实例,因此您会收到有关未知键的异常(nil没有您为ShareView定义的可输出属性)。
您应该将xib文件的加载器定义为所有者(即负责加载xib文件的视图控制器)。然后在xib中添加单独的UIView对象,并将其定义为ShareView的实例。然后再加载xib文件。
ShareView *shareView = [[[[NSBundle mainBundle] loadNibNamed:@"ShareView" owner:self options:nil] objectAtIndex:0] retain];

你还可以在视图控制器的接口中将shareView定义为IBOutlet(并将该插座从文件所有者连接到xib中的该视图)。然后当你加载xib时,就不需要重新分配shareView实例变量,因为xib加载过程将直接重新连接视图到实例变量。


我按照你说的做了(部分地),但还是不起作用。我说部分地,因为我不明白你的意思是什么:“然后将单独的UIView对象添加到xib中,并将其定义为ShareView的实例。”现在我得到了NSUnknownException错误,因为“MainController”没有实现ShareView中的outlets。这就是为什么我不理解你的答案。既然ShareView的逻辑位于ShareView.m中,而Interface Builder中ShareView的类标识设置为ShareView,为什么“MainController”应该是shareView的所有者呢?希望这样有点意义。 - ThomasM
1
以下是创建xib的步骤:1)从“文件”->“新建”对话框中创建一个视图xib。2)将MainController定义为文件所有者。3)在xib中选择视图对象,并将其设置为ShareView的子类。4)双击该视图并根据您的需要进行编辑。5)将该视图对象中的插座连接到您添加的子视图。 - Eimantas
1
你需要在MainController中放置一个动作,并将IBAction输出连接从按钮连接到文件所有者。 - Eimantas
我认为这违反了MVC的原则。视图不仅要负责呈现,还要执行一些操作(通常是控制器的职责)。 - Eimantas
1
当然这是一个选项。但我想你和我一样知道,在不同的视图控制器中重复代码是不可取的...如果我想在那个IBAction中改变一些东西怎么办? - ThomasM
显示剩余4条评论

4
我想要补充一下答案。希望人们能够改进这个答案。
首先,它是有效的。
XIB:
结果:
我很久以前就想为UIView创造一个子类,特别是用于tableViewCell。
这就是我做的方式。
它成功了,但在我看来,有些部分仍然有点“笨拙”。
首先,我创建了一个通常的.h、.m和xib文件。请注意,如果您创建的子类不是UIViewController的子类,苹果没有复选框可以自动创建xib。无论如何,都要创建它们。
#import <UIKit/UIKit.h>
#import "Business.h"

@interface BGUIBusinessCellForDisplay : UITableViewCell

+ (NSString *) reuseIdentifier;


- (BGUIBusinessCellForDisplay *) initWithBiz: (Business *) biz;
@end

我有一个非常简单的UITableViewCell,稍后我想用biz初始化它。

我设置了重用标识符,这是为UITableViewCell应该做的事情。

//#import "Business.h"
@interface BGUIBusinessCellForDisplay ()
@property (weak, nonatomic) IBOutlet UILabel *Title;
@property (weak, nonatomic) IBOutlet UIImageView *Image;
@property (weak, nonatomic) IBOutlet UILabel *Address;
@property (weak, nonatomic) IBOutlet UILabel *DistanceLabel;
@property (weak, nonatomic) IBOutlet UILabel *PinNumber;
@property (strong, nonatomic) IBOutlet BGUIBusinessCellForDisplay *view;

@property (weak, nonatomic) IBOutlet UIImageView *ArrowDirection;
@property (weak, nonatomic) Business * biz;
@end

@implementation BGUIBusinessCellForDisplay

- (NSString *) reuseIdentifier {
    return [[self class] reuseIdentifier];
};
+ (NSString *) reuseIdentifier {
    return NSStringFromClass([self class]);
};

然后我删除了大部分的初始化代码,并使用以下代码代替:
- (BGUIBusinessCellForDisplay *) initWithBiz: (Business *) biz
{
    if (self.biz == nil) //First time set up
    {
        self = [super init]; //If use dequeueReusableCellWithIdentifier then I shouldn't change the address self points to right
        NSString * className = NSStringFromClass([self class]);
        //PO (className);
        [[NSBundle mainBundle] loadNibNamed:className owner:self options:nil];
        [self addSubview:self.view]; //What is this for? self.view is of type BGCRBusinessForDisplay2. That view should be self, not one of it's subview Things don't work without it though
    }
    if (biz==nil)
    {
        return self;
    }

    self.biz = biz;
    self.Title.text = biz.Title; //Let's set this one thing first
    self.Address.text=biz.ShortenedAddress;

    //if([self.distance isNotEmpty]){
    self.DistanceLabel.text=[NSString stringWithFormat:@"%dm",[biz.Distance intValue]];
    self.PinNumber.text =biz.StringPinLineAndNumber;

请注意,这真的很尴尬。

首先,init有两种用法。

  1. 它可以在aloc之后使用。
  2. 我们可以使用另一个现有的类,然后只想将该现有单元格初始化为另一个业务。

所以我做了:

if (self.biz == nil) //First time set up
{
    self = [super init]; //If use dequeueReusableCellWithIdentifier then I shouldn't change the address self points to right
    NSString * className = NSStringFromClass([self class]);
    //PO (className);
    [[NSBundle mainBundle] loadNibNamed:className owner:self options:nil];
    [self addSubview:self.view]; //What is this for? self.view is of type BGCRBusinessForDisplay2. That view should be self, not one of it's subview Things don't work without it though
}

我做的另一件不好的事情是当我执行 [self addSubview:self.view]; 时。

问题是我想要 self 成为 唯一的 视图。而不是 self.view。但不知何故,它仍然能够工作。所以,是的,请帮助我改进,但这基本上就是实现自己的 UIView 子类的方法。


我们发现按照正常方式构建NIB并像这个答案一样实例化它更简单:http://stackoverflow.com/questions/9014105/where-is-a-good-tutorial-for-making-a-custom-uitableviewcell - Rembrandt Q. Einstein

2
您可以使用IB_DESIGNABLE在新的Xcode 6中创建自定义的UIView并设计xib,甚至可以使Interface Builder在其他xib文件或storyboard中显示它。在xib中将文件所有者设置为您的自定义类,但不要设置UIView类以避免循环加载问题。只需保留默认的UIView类,并将此UIView作为您的自定义类视图的子视图添加即可。将所有输出连接到文件所有者,在您的自定义类中像下面的代码一样加载您的xib。您可以在这里查看我的视频教程。
IB_DESIGNABLE
@interface CustomControl : UIView
@end

@implementation CustomControl

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        [self load];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        [self load];
    }
    return self;
}

- (void)load
{
    UIView *view = [[[NSBundle bundleForClass:[self class]] loadNibNamed:@"CustomControl" owner:self options:nil] firstObject];
    [self addSubview:view];
    view.frame = self.bounds;
}

@end

如果您正在使用自动布局,则可能需要更改以下内容:view.frame = self.bounds; 改为:
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view)]];

1
要在Auto-Layout中使用Yang's pattern,您需要在-awakeWithCoder:方法中的某个位置添加以下内容。
    theRealThing.translatesAutoresizingMaskIntoConstraints = NO;

如果您不关闭-translatesAutoResizingMaskIntoConstraints,它可能会导致布局不正确,并在控制台中引起大量的调试错误信息。
编辑:自动布局仍然可能很麻烦。某些约束条件不被尊重,但其他约束条件是被尊重的(例如,固定到底部不起作用,但固定到顶部则起作用)。我们不确定原因,但您可以通过手动从占位符传递约束条件到真实元素来解决此问题。
还值得注意的是,这种模式在故事板和常规.xib文件中同样适用(即,您可以在.xib文件中创建UI元素,并通过遵循您的步骤将其放入故事板视图控制器中)。

-1

为什么不继承UIViewController而是UIView?请查看以下链接。在其中创建了一个“RedView”和“BlueView”的UIViewControllers,它们有自己的xib,并通过创建前两个类的实例并在MultipleViewsController的viewDidLoad方法中添加[self.view addSubview:red.view][self.view addSubview:blue.view]将它们添加到MultipleViewsController视图中。

一个视图中的多个控制器

只需在上述链接的代码中的RedView和BlueView的按钮按下函数中添加(id)sender即可。


就像我在问题中所说的:“我在几个地方读到,ViewControllers仅用于管理全屏幕,即不是屏幕的一部分…” - ThomasM
在您的问题中,您正在使用xib创建视图,并通过addSubview方法将其添加到主视图中。在我给您的链接中,我制作了两个控制器,并将redView和blueView添加到同一个视图中。两者都由自己的控制器控制。您可以在它们各自的类中添加逻辑。此外,您还可以使用界面构建器修改这两个视图。 - HG's
啊,我现在明白了。但是那种方法似乎不被 Apple 支持。如果你查看文档,它说:“您不应该使用多个自定义视图控制器来管理同一视图层次结构的不同部分。 同样,您也不应该使用单个自定义视图控制器对象来管理多个屏幕的内容。”http://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/AboutViewControllers/AboutViewControllers.html#//apple_ref/doc/uid/TP40007457-CH112-SW10 - ThomasM
好的,谢谢你指出来。我不知道它不被苹果支持 :)。不过它还是能用的。 - HG's

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