使用 xib 创建可重用的 UIView(并从情节提要中加载)

Posted

技术标签:

【中文标题】使用 xib 创建可重用的 UIView(并从情节提要中加载)【英文标题】:Creating a reusable UIView with xib (and loading from storyboard) 【发布时间】:2014-03-20 19:52:57 【问题描述】:

好的,*** 上有很多关于这个的帖子,但没有一个对解决方案特别清楚。我想创建一个自定义 UIView 并附带一个 xib 文件。要求是:

没有单独的UIViewController – 一个完全独立的类 类中的插座允许我设置/获取视图的属性

我目前的做法是:

    覆盖-(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame 
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    
    

    在我的视图控制器中使用-(id)initWithFrame: 以编程方式实例化

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

这很好用(尽管从不调用 [super init] 并简单地使用加载的 nib 的内容设置对象似乎有点可疑 - 这里有对 add a subview in this case 的建议,它也很好用)。但是,我也希望能够从情节提要中实例化视图。所以我可以:

    在情节提要的父视图上放置UIView 将其自定义类设置为MyCustomView

    覆盖-(id)initWithCoder:——我最常看到的代码符合如下模式:

    -(id)initWithCoder:(NSCoder *)aDecoder 
        self = [super initWithCoder:aDecoder];
        if (self) 
            [self initializeSubviews];
        
        return self;
    
    
    -(id)initWithFrame:(CGRect)frame 
        self = [super initWithFrame:frame];
        if (self) 
            [self initializeSubviews];
        
        return self;
    
    
    -(void)initializeSubviews 
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    
    

当然,这不起作用,因为无论我是使用上述方法,还是以编程方式进行实例化,最终都会在输入 -(void)initializeSubviews 并从文件加载 nib 时递归调用 -(id)initWithCoder:

其他几个 SO 问题处理此问题,例如 here、here、here 和 here。但是,给出的答案都不能令人满意地解决问题:

一个常见的建议似乎是将整个类嵌入到 UIViewController 中,并在那里加载 nib,但这对我来说似乎不是最理想的,因为它需要添加另一个文件作为包装器

任何人都可以就如何解决此问题提供建议,并在自定义UIView 中使用最少的麻烦/没有瘦控制器包装器的工作插座?或者有没有另一种更简洁的方式来使用最少的样板代码?

【问题讨论】:

你有没有得到满意的答案?我目前正在为此而苦苦挣扎。正如您所提到的,所有其他答案似乎都不够好。如果您在过去几个月中发现了任何问题,您总是可以自己回答问题。 为什么在 ios 中创建可重用视图如此困难? 谢谢@Suragch! There's also an answer which details how to do this in Swift further down the page 事实上,您链接到的答案使用完全相同的方法(尽管您的答案不包括来自 rect 函数的 init,这意味着它只能从情节提要初始化而不能以编程方式)跨度> 关于这个非常古老的 QA,Apple 终于推出了 STORYBOARD REFERENCES ... developer.apple.com/library/ios/recipes/… ... 就是这样,唷! 【参考方案1】:

请注意,这个 QA(与许多人一样)实际上只是具有历史意义。

现在多年来,iOS 中的一切都只是一个容器视图。 Full tutorial here

(事实上,Apple 终于在不久前添加了Storyboard References,让它变得更容易了。)

这是一个典型的故事板,随处可见容器视图。一切都是容器视图。这就是您制作应用程序的方式。

(出于好奇,KenC 的回答准确地显示了过去如何将 xib 加载到一种包装器视图中,因为您不能真正“分配给自己”。)

【讨论】:

这样做的问题是,您最终会为所有嵌入的内容视图创建大量 ViewController。 嗨@BogdanOnu!你应该有很多很多的视图控制器。对于“最小”的东西 - 你应该有一个视图控制器。 这非常有用——感谢@JoeBlow。使用容器视图显然是一种替代方法,也是一种避免直接处理 xib 的所有复杂性的简单方法。但是,创建用于跨项目分发/使用的可重用组件似乎并不是 100% 令人满意的替代方案,因为它需要将所有 UI 设计直接嵌入到故事板中。 在这种情况下使用额外的 ViewController 问题不大,因为它只包含在 xib 案例中属于自定义 View 类的程序逻辑,但是紧密耦合使用故事板意味着我不确定容器视图是否可以完全解决这个问题。也许在大多数实际情况下,视图是特定于项目的,因此这是最好和最“标准”的解决方案,但我很惊讶仍然没有简单的方法来打包自定义视图以供故事板使用。我的程序员分而治之的冲动是很痒;) 另外,随着 Xcode 6 中自定义 UIView 子类的实时渲染的引入,我不确定我是否相信以这种方式使用 xibs 创建视图的前提现在已被弃用【参考方案2】:

我将此作为单独的帖子添加,以更新 Swift 发布的情况。 LeoNatan 描述的方法在 Objective-C 中完美运行。但是,在 Swift 中从 xib 文件加载时,更严格的编译时检查会阻止分配 self

因此,别无选择,只能将从 xib 文件加载的视图添加为自定义 UIView 子类的子视图,而不是完全替换 self。这类似于原始问题中概述的第二种方法。使用这种方法的 Swift 中的一个类的大致轮廓如下:

@IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView 

    required init(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        initializeSubviews()
    

    override init(frame: CGRect) 
        super.init(frame: frame)
        initializeSubviews()
    

    func initializeSubviews() 
        // below doesn't work as returned class name is normally in project module scope
        /*let viewName = NSStringFromClass(self.classForCoder)*/
        let viewName = "ExampleView"
        let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                               owner: self, options: nil)[0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    


这种方法的缺点是在视图层次结构中引入了一个额外的冗余层,当使用 LeoNatan 在 Objective-C 中概述的方法时不存在这种冗余层。然而,这可能被视为一种必要的邪恶,也是 Xcode 中设计事物的基本方式的产物(在我看来仍然很疯狂,很难以一致的方式将自定义 UIView 类与 UI 布局链接起来在故事板和代码中)——在初始化程序中替换self批发之前似乎从来都不是一种特别可解释的做事方式,尽管每个视图基本上有两个视图类似乎也不是那么好。

尽管如此,这种方法的一个令人高兴的结果是,我们不再需要在界面构建器中将视图的自定义类设置为我们的类文件,以确保在分配给 self 时的行为正确,因此递归调用 init(coder aDecoder: NSCoder)当发出loadNibNamed() 被破坏时(通过不在xib 文件中设置自定义类,将调用普通UIView 的init(coder aDecoder: NSCoder) 而不是我们的自定义版本)。

即使我们不能直接对存储在 xib 中的视图进行类自定义,在将视图的文件所有者设置为自定义类:

可以在in the following video找到一个演示使用这种方法逐步实现这种视图类的视频。

【讨论】:

嗨,乔——感谢您的 cmets,这很大胆(就像您的许多其他 cmets 一样!)正如我在回复您的回答时所说,我同意在大多数情况下容器视图可能是最好的方法,但是在必须跨项目(或分布式)使用视图的情况下,至少对我来说确实有意义,而且对其他人来说似乎也有替代方案。您可能个人认为这是不好的风格,但也许您可以让这里的许多其他帖子建议如何做到这一点以供参考,并让人们自己判断。这两种方法似乎都很有用。 谢谢。我在 Swift 中尝试了许多不同的方法,但都没有成功,直到我听从了您关于将 nib 的类保留为 UIView 的建议。我同意 Apple 从来没有让这件事变得简单是疯狂的,现在这几乎是不可能的。容器并不总是答案。【参考方案3】:

步骤 1。从情节提要替换 self

initWithCoder: 方法中替换self 将失败并出现以下错误。

'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'

相反,您可以用awakeAfterUsingCoder:(而不是awakeFromNib)替换解码的对象。喜欢:

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder 
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];

@end

步骤 2。防止递归调用

当然,这也会导致递归调用问题。 (故事板解码 -> awakeAfterUsingCoder: -> loadNibNamed: -> awakeAfterUsingCoder: -> loadNibNamed: -> ...) 所以你必须检查当前 awakeAfterUsingCoder: 在 Storyboard 解码过程或 XIB 解码过程中是否被调用。 你有几种方法可以做到这一点:

a) 使用仅在 NIB 中设置的私有 @property

@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end

并仅在“MyCustomView.xib”中设置“用户定义的运行时属性”。

优点:

缺点:

根本不起作用:setXib: 将被称为 AFTER awakeAfterUsingCoder:

b) 检查self 是否有任何子视图

通常,您在 xib 中有子视图,但在情节提要中没有。

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder 
    if(self.subviews.count > 0) 
        // loading xib
        return self;
    
    else 
        // loading storyboard
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    

优点:

Interface Builder 中没有任何技巧。

缺点:

故事板中不能有子视图。

c) 在loadNibNamed: 调用期间设置静态标志

static BOOL _loadingXib = NO;

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder 
    if(_loadingXib) 
        // xib
        return self;
    
    else 
        // storyboard
        _loadingXib = YES;
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        _loadingXib = NO;
        return view;
    

优点:

简单 Interface Builder 中没有任何技巧。

缺点:

不安全:静态共享标志很危险

d) 在 XIB 中使用私有子类

例如,将_NIB_MyCustomView 声明为MyCustomView 的子类。 并且,仅在您的 XIB 中使用 _NIB_MyCustomView 而不是 MyCustomView

MyCustomView.h:

@interface MyCustomView : UIView
@end

MyCustomView.m:

#import "MyCustomView.h"

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder 
    // In Storyboard decoding path.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];

@end

@interface _NIB_MyCustomView : MyCustomView
@end

@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder 
    // In XIB decoding path.
    // Block recursive call.
    return self;

@end

优点:

MyCustomView 中没有明确的if

缺点:

在 xib Interface Builder 中添加 _NIB_ 前缀 相对较多的代码

e) 在 Storyboard 中使用子类作为占位符

类似于d),但在 Storyboard 中使用子类,在 XIB 中使用原始类。

在这里,我们将MyCustomViewProto 声明为MyCustomView 的子类。

@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder 
    // In storyboard decoding
    // Returns MyCustomView loaded from NIB.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                          owner:nil
                                        options:nil] objectAtIndex:0];

@end

优点:

非常安全 清洁; MyCustomView 中没有额外的代码。 没有明确的if 检查与d) 相同

缺点:

需要在情节提要中使用子类。

我认为e) 是最安全、最干净的策略。所以我们在这里采用它。

步骤 3。复制属性

在'awakeAfterUsingCoder:'中的loadNibNamed: 之后,您必须从self 复制几个属性,这是故事板的解码实例。 frame 和 autolayout/autoresize 属性尤其重要。

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder 
    typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                       owner:nil
                                                     options:nil] objectAtIndex:0];
    // copy layout properities.
    view.frame = self.frame;
    view.autoresizingMask = self.autoresizingMask;
    view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;

    // copy autolayout constraints
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in self.constraints) 
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == self) firstItem = view;
        if(secondItem == self) secondItem = view;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    

    // move subviews
    for(UIView *subview in self.subviews) 
        [view addSubview:subview];
    
    [view addConstraints:constraints];

    // Copy more properties you like to expose in Storyboard.

    return view;

最终解决方案

如您所见,这是一些样板代码。我们可以将它们实现为“类别”。 在这里,我扩展了常用的UIView+loadFromNib代码。

#import <UIKit/UIKit.h>

@interface UIView (loadFromNib)
@end

@implementation UIView (loadFromNib)

+ (id)loadFromNib 
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                          owner:nil
                                        options:nil] objectAtIndex:0];


- (void)copyPropertiesFromPrototype:(UIView *)proto 
    self.frame = proto.frame;
    self.autoresizingMask = proto.autoresizingMask;
    self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in proto.constraints) 
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == proto) firstItem = self;
        if(secondItem == proto) secondItem = self;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    
    for(UIView *subview in proto.subviews) 
        [self addSubview:subview];
    
    [self addConstraints:constraints];

使用这个,你可以声明MyCustomViewProto like:

@interface MyCustomViewProto : MyCustomView
@end

@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder 
    MyCustomView *view = [MyCustomView loadFromNib];
    [view copyPropertiesFromPrototype:self];

    // copy additional properties as you like.

    return view;

@end

XIB:

故事板:

结果:

【讨论】:

解决方案比最初的问题更复杂。要停止递归循环,您只需设置 File's Owner 对象,而不是将内容视图声明为 MyCustomView 类类型。 这只是a)简单的初始化过程但复杂的视图层次结构和b)复杂的初始化过程但简单的视图层次结构的权衡。 n'est-ce过去了吗? ;) 这个项目有下载链接吗?【参考方案4】:

您的问题是从initWithCoder:(的后代)调用loadNibNamed:loadNibNamed: 内部调用 initWithCoder:。如果您想覆盖情节提要编码器,并始终加载您的 xib 实现,我建议使用以下技术。将一个属性添加到您的视图类,并在 xib 文件中,将其设置为预定值(在用户定义的运行时属性中)。现在,在调用[super initWithCoder:aDecoder]; 之后检查属性的值。如果是预定值,请不要调用[self initializeSubviews];

所以,是这样的:

-(instancetype)initWithCoder:(NSCoder *)aDecoder 
    self = [super initWithCoder:aDecoder];

    if (self && self._xibProperty != 666)
    
        //We are in the storyboard code path. Initialize from the xib.
        self = [self initializeSubviews];

        //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
        //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
    

    return self;


-(instancetype)initializeSubviews 
    id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];

    return view;

【讨论】:

感谢@LeoNatan!我接受这个答案,因为它是最初陈述的问题的最佳解决方案。但是,请注意,在 Swift 中不再可能 - 我已经添加了一些单独的注释,说明在这种情况下可能的解决方法。 @KenChatfield 我在我的 Swift 子项目中注意到了这一点,并对此感到恼火。我不确定他们在想什么,因为没有这个,很多 Cocoa/Cocoa Touch 内部实现在 Swift 中是不可能的。我敢打赌,当他们真正有时间专注于功能而不是错误时,将会有一些动态功能。 Swift 根本没有准备好,最糟糕的是开发工具。 这不是 hack,而是类集群的工作方式。实际上,允许返回作为返回类的子类的对象的返回并不存在技术问题。事实上,Cocoa 和 Cocoa Touch 的基石之一——类集群——是不可能实现的。糟糕的。一些框架,比如 Core Data,不能在 Swift 中实现,这使得它在我的大部分应用中都是无用的语言。 不知何故它对我不起作用(iOS8.1 SDK)。我在 XIB 中设置了一个 restoreIdentifier 而不是运行时属性,而不是它的工作。例如,我在 xib 中设置了“MyViewRestorationID”,而不是在 initWithCoder 中:我检查了 ![[self restoreIdentifier] isEqualToString:@"MyViewRestorationID"] 这不起作用,至少在 iOS 10 上,因为“用户定义的运行时属性”的绑定在 -initWIthCoder: 完成之前不会发生。【参考方案5】:

别忘了

两个重点:

    将 .xib 的文件所有者设置为自定义视图的类名。 不要在 IB 中为 .xib 的根视图设置自定义类名。

在学习制作可重用视图时,我多次访问此问答页面。忘记以上几点让我浪费了很多时间试图找出导致无限递归发生的原因。这些点在此处的其他答案和elsewhere 中有所提及,但我只想在这里再次强调。

我的完整 Swift 步骤答案是 here。

【讨论】:

这是我的答案,我已经设置了 BOTH - 你只需要文件所有者,如果你将视图的主要内容设置为自定义类,它会创建一个无限循环【参考方案6】:

有一个比上述解决方案更清洁的解决方案: https://www.youtube.com/watch?v=xP7YvdlnHfA

没有运行时属性,完全没有递归调用问题。 我尝试了它,它使用来自故事板和具有 IBOutlet 属性(iOS8.1、XCode6)的 XIB 就像一个魅力。

祝你编码好运!

【讨论】:

谢谢@ingaham!但是,视频中概述的方法与原始问题中提出的第二种解决方案相同(我在上面的答案中提供了 Swift 代码)。在这两种情况下,它都涉及向包装 UIView 子类添加子视图,这就是为什么递归调用没有问题,也不需要依赖运行时属性或其他任何复杂的东西。如前所述,缺点是需要将多余的附加子视图添加到自定义 UIView 类中。正如所讨论的,这可能是目前最好和最简单的解决方案。 是的,你完全正确,这些是相同的解决方案。但是,冗余视图是必要的,但它是最干净且易于维护的解决方案。所以我决定用这个。 我相信冗余视图是完全自然的,永远不会有其他解决方案。请注意,您是在说 "'something' will go 'here'" ...“here”是自然存在的事物。那里必须有“东西”,一个“你要放置东西的地方”——这就是“视图”的定义。等等! Apple 的“容器视图”的东西确实就是这样 .. 有一个“框架”,一个“持有者视图”(“容器视图”),您可以将某些东西放入其中。事实上,“冗余”视图解决方案正是手工制作的容器视图!只需使用 Apple 的。

以上是关于使用 xib 创建可重用的 UIView(并从情节提要中加载)的主要内容,如果未能解决你的问题,请参考以下文章

如何从 xib 文件 uiview 到情节提要 ViewController

Storyboard xib 实时视图在左上角呈现所有子视图

如何在情节提要场景中嵌入自定义视图 xib?

尽管已正确初始化,XIB 视图仍不显示任何内容

设计一个 XIB 并在不同的 UIViewController 中重用它

使用自定义 uiview xib