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


81

好的,关于此的StackOverflow上有很多帖子,但是在解决方案上没有一个特别明确的地方。我想UIView用随附的xib文件创建一个自定义。要求是:

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

我目前执行此操作的方法是:

  1. 覆写 -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  2. -(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]并且仅使用已加载的笔尖的内容设置对象似乎有点可疑–在这种情况下,建议在此添加一个子视图也可以)。但是,我也希望能够从情节提要中实例化视图。所以我可以:

  1. 放置UIView在故事板父视图
  2. 将其自定义类设置为 MyCustomView
  3. 覆盖-(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];
    }
    

当然,这是行不通的,因为无论我使用上面的方法还是以编程方式实例化,两者都最终-(id)initWithCoder:在输入-(void)initializeSubviews并从文件中加载笔尖时递归调用。

其他一些SO问题也处理此问题,例如此处此处此处此处。但是,给出的答案都不能令人满意地解决该问题:

  • 一个常见的建议似乎是将整个类嵌入UIViewController中,然后在其中进行nib加载,但这对我来说似乎不太理想,因为它需要添加另一个文件作为包装器

谁能提供有关如何解决此问题的建议,并UIView以最小的麻烦/没有薄控制器包装的习惯获得工作插座?还是有另一种更清洁的方式以最少的样板代码进行工作?


1
您是否为此得到满意的答复?我目前正在为此奋斗。正如您提到的,所有其他答案似乎还不够好。如果您在过去的几个月中发现任何问题,可以随时亲自回答该问题。
Mike Meyers 2014年

13
为什么在iOS中创建可重用视图如此困难?
Clocksmith 2014年


1
实际上,您链接到的答案使用完全相同的方法(尽管您的答案不包括来自rect函数的init,这意味着只能从情节提要中初始化它,而不能以编程方式对其进行初始化)
Ken Chatfield

1
关于这个非常古老的质量检查,苹果终于推出了故事板参考书... developer.apple.com/library/ios/recipes / ... ...就是这样,!
Fattie

Answers:


13

您的问题是loadNibNamed:从(的后代)发出的initWithCoder: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子项目中注意到了这一点,并对此感到恼火。我不确定他们在想什么,因为没有这个,在Swift中很多Cocoa / Cocoa Touch内部实现是不可能的。我敢打赌,当它们实际上有时间专注于功能而不是错误时,将会有一些动态功能。Swift根本还没有准备好,最坏的犯罪者是开发工具。
Leo Natan 2014年

是的,您是对的,Swift现在有很多粗糙的边缘。话虽如此,似乎直接分配给self似乎有点麻烦,所以我很高兴以某种方式编译器现在警告此类情况(这种更严格的类型检查似乎是关于语言)。但是,您说对了,它使自定义视图与xib的链接非常混乱并且有些不令人满意。让我们希望,正如您所说的那样,一旦他们完成了对bug的整理后,我们将看到更多的动态功能,以帮助我们更多地利用诸如此类的东西!
肯·查菲尔德

这不是黑客,而是类集群的工作方式。实际上,没有技术问题可以允许返回作为返回类的子类的对象的返回。实际上,可可和Cocoa Touch的基石之一-类集群-不可能实现。可怕。某些框架(例如Core Data)无法在Swift中实现,这使它成为我大部分使用的无用语言。
Leo Natan 2014年

1
不知怎么对我不起作用(iOS8.1 SDK)。我在XIB中设置了restoreIdentifier,而不是在运行时属性中设置了它,但是它起作用了。例如,我在xib中设置了“ MyViewRestorationID”,而不是在initWithCoder中设置了:我检查了[[self restoreIdentifier] isEqualToString:@“ MyViewRestorationID”]
ingaham 2015年

26

请注意,此质量检查(与许多质量检查一样)实际上只是历史性的。

如今多年来,iOS中的所有内容都只是一个容器视图。完整的教程在这里

(实际上,Apple终于在不久前添加了Storyboard References,使其变得更加容易。)

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

在此处输入图片说明

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


这样做的问题是,对于所有嵌入的内容视图,最终都会有很多ViewController。
Bogdan Onu 2014年

嗨@BogdanOnu!您应该有很多很多视图控制器。对于“最小”的东西-您应该有一个视图控制器。
Fattie 2014年

2
这非常有用–感谢@JoeBlow。使用容器视图绝对是一种替代方法,并且是避免直接处理xib的所有复杂操作的简单方法。但是,创建可重复使用的组件以在项目之间分发/使用似乎不是100%令人满意的替代方案,因为它要求所有UI设计都直接嵌入故事板。
肯·查菲尔德

3
在这种情况下,使用额外的ViewController不会有什么问题,因为它只会包含程序逻辑,而该逻辑应属于xib情况下的自定义View类,但是与情节提要的紧密结合意味着我不确定容器视图能否完全解决此问题。也许在大多数实际情况下,视图是特定于项目的,因此这是最好和最“标准”的解决方案,但是令人惊讶的是,仍然没有简便的方法来打包自定义视图以供情节提要使用。我的程序员想将其分解并征服为孤立的组件的冲动是发痒的;)
肯·查菲尔德

1
另外,随着Xcode 6中自定义UIView子类的实时渲染的引入,我不确定是否要以这样的方式使用xibs创建视图的前提是否已被弃用
Ken Chatfield

24

我将其作为单独的帖子添加,以通过Swift的发布来更新情况。LeoNatan描述的方法在Objective-C中非常有效。但是,self从Swift中的xib文件加载时,更严格的编译时间检查会阻止分配给它们。

结果,别无选择,只能将从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文件中的自定义类,将调用init(coder aDecoder: NSCoder)普通香草UIView而非我们的自定义版本)。

即使我们不能直接对存储在xib中的视图进行类自定义,但在将视图的文件所有者设置为我们的自定义类之后,我们仍然可以使用出口/操作等将视图链接到“父” UIView子类。

设置自定义视图的文件所有者属性

在下面的视频中可以找到一个视频,演示了使用此方法逐步实现这种视图类的实现。


嗨,乔-谢谢您的评论,这非常大胆(与您的其他评论一样!)正如我在回答您的回答时所说,我同意在大多数情况下容器视图可能是最好的方法,但是在这种情况下在必须跨项目(或分布式)使用视图的地方,这至少对我来说是有意义的,对其他人来说似乎也可以选择。您可能个人认为这是不好的作风,但是也许您可以在这里发表许多其他文章,建议如何做以供参考,并让人们自行判断。两种方法似乎都是有用的。
肯查特菲尔德,2015年

谢谢你 我在Swift中尝试了多种方法,但都没有成功,直到我听取了您关于将笔尖的类保留为的建议UIView。我同意苹果公司从来没有做到这一点很疯狂,现在几乎是不可能的。容器并不总是答案。
梯队2015年

16

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

更换selfinitWithCoder:方法将失败,下面的错误。

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

相反,您可以将解码的对象替换为awakeAfterUsingCoder:(not 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:是在故事板解码过程还是XIB解码过程中被调用。您有几种方法可以做到这一点:

a)使用@property仅在NIB中设置的private 。

@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

优点:

  • 没有明确ifMyCustomView

缺点:

  • _NIB_xib Interface Builder中的前缀技巧
  • 相对更多的代码

e)使用子类作为情节提要中的占位符

类似于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)是最安全,最清洁的策略。所以我们在这里采用。

第三步 复制属性

之后loadNibNamed:在“awakeAfterUsingCoder:”,你必须复制从几个属性self被解码例如F中的故事板。frame和自动布局/自动调整大小属性特别重要。

- (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

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

XIB屏幕截图

故事板:

故事板

结果:

在此处输入图片说明


3
解决方案比最初的问题更复杂。要停止递归循环,您只需要设置File的Owner对象,而不是将内容视图声明为MyCustomView类类型。
Bogdan Onu 2014年

这只是a)简单的初始化过程但复杂的视图层次结构和b)复杂的初始化过程但简单的视图层次结构之间的权衡。n'est-ce pas?;)
rintaro 2014年

该项目有任何下载链接吗?
karthikeyan 2015年

13

别忘了

两个要点:

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

在学习制作可重用的视图时,我多次访问了此问答页面。忘记了以上几点,使我浪费大量时间试图找出导致无限递归发生的原因。这些要点在此处和其他地方的其他答案中都已提及,但是我只想在这里再次强调它们。

我对Swift的完整回答(包括步骤)在这里


2

有一个解决方案比上面的解决方案干净得多:https : //www.youtube.com/watch?v=xP7YvdlnHfA

没有运行时属性,根本没有递归调用问题。我尝试了一下,将其从情节提要和带有IBOutlet属性(iOS8.1,XCode6)的XIB一起使用时,就像是一种魅力。

祝您编码愉快!


1
谢谢@ingaham!但是,视频中概述的方法与原始问题中提出的第二个解决方案相同(上面的答案中提供了Swift代码)。在这两种情况下,都需要向包装UIView子类中添加一个子视图,这就是为什么递归调用没有问题,也不需要依赖运行时属性或其他任何复杂问题的原因。如前所述,缺点是需要将多余的附加子视图添加到自定义UIView类。正如您所讨论的那样,这可能是目前最好和最简单的解决方案。
肯查特菲尔德

是的,您完全正确,这些是相同的解决方案。但是,冗余视图是必需的,但这是最干净,可维护性最好的解决方案。所以我决定使用这个。
ingaham 2015年

我认为,多余的观点是完全自然的,永远不可能有其他解决方案。请注意,您说的是“某物会在这里”, ……“这里”是自然存在的事物。那里只需要有一个“东西”,一个“您要放置东西的地方”,即“视图”的定义。等一下 实际上, Apple的“容器视图”内容就是..其中存在一个“框架”,一个“持有者视图”(“容器视图”),您可以将其中的内容塞入其中。确实,“冗余”视图解决方案就是手工制作的容器视图!只需使用苹果的。
Fattie
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.