使用xib创建可重用的UIView(并从storyboard加载)

Ken*_*eld 79 objective-c storyboard uiview nib ios

好的,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;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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];
    
    Run Code Online (Sandbox Code Playgroud)

这工作正常(虽然从来没有调用[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];
    }
    
    Run Code Online (Sandbox Code Playgroud)

当然,这不起作用,因为我是否使用上面的方法,或者我是否以编程方式实例化,最终都以递归方式调用从文件-(id)initWithCoder:输入-(void)initializeSubviews和加载nib.

其他几个SO问题就像这里,这里,这里这里一样.但是,没有给出令人满意的答案解决问题:

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

任何人都可以提供有关如何解决此问题的建议,并在自定义中获得工作插座UIView,最小化/没有薄控制器包装?或者是否有一种替代的,更清洁的方式来处理最少的样板代码?

Fat*_*tie 26

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

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

(事实上​​,Apple 现在不久前添加了Storyboard References,使其变得更加容易.)

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

在此输入图像描述

(作为一种好奇心,KenC的答案确切地说明了如何将xib加载到一种包装器视图中,因为你无法真正"分配给自己".)

  • 在这种情况下使用额外的ViewController我没有遇到什么问题,因为它只包含在xib情况下属于自定义View类的程序逻辑,但与故事板的紧密耦合意味着我是不确定容器视图可以完全解决这个问题.也许在大多数实际情况下,视图都是特定于项目的,因此这是最好和最"标准"的解决方案,但我很惊讶仍然没有简单的方法来打包自定义视图以供故事板使用.我的程序员分裂和征服成孤立组件的冲动是瘙痒;) (3认同)
  • 这非常有用 - 谢谢@JoeBlow.使用容器视图肯定是一种替代方法,并且是避免直接处理xib的所有复杂问题的简单方法.但是,它似乎不是100%令人满意的替代方案,可以创建可重用的组件以供跨项目分发/使用,因为它需要将所有UI设计直接嵌入到故事板中. (2认同)

Ken*_*eld 23

我将此作为一个单独的帖子添加,以更新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
    }

}
Run Code Online (Sandbox Code Playgroud)

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

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

即使我们不能直接对存储在xib中的视图进行类自定义,我们仍然可以在将视图的文件所有者设置为自定义类之后,使用outlet/actions等将视图链接到我们的"父"UIView子类:

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

可以在以下视频中找到使用此方法逐步演示此类视图类的实现的视频.


rin*_*aro 16

步骤1.self从故事板替换

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

'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
Run Code Online (Sandbox Code Playgroud)

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

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end
Run Code Online (Sandbox Code Playgroud)

第2步.防止递归调用

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

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

@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end
Run Code Online (Sandbox Code Playgroud)

并在"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];
    }
}
Run Code Online (Sandbox Code Playgroud)

优点:

  • 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

优点:

  • 简单
  • Interface Builder中没有技巧.

缺点:

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

d)在XIB中使用私有子类

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

MyCustomView.h:

@interface MyCustomView : UIView
@end
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

优点:

  • 没有明确ifMyCustomView

缺点:

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

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
Run Code Online (Sandbox Code Playgroud)

优点:

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

缺点:

  • 需要在storyboard中使用子类.

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

STEP3.复制属性

之后loadNibNamed:在"awakeAfterUsingCoder:",你必须复制从几个属性self被解码例如F中的故事板.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;
}
Run Code Online (Sandbox Code Playgroud)

最终解决方案

如您所见,这是一些样板代码.我们可以将它们实现为"类别".在这里,我扩展了常用的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];
}
Run Code Online (Sandbox Code Playgroud)

使用此,您可以声明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
Run Code Online (Sandbox Code Playgroud)

厦门国际银行:

XIB截图

故事板:

故事板

结果:

在此输入图像描述

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

Leo*_*ica 13

你的问题是loadNibNamed:来自(后代)initWithCoder:.loadNibNamed:内部呼叫initWithCoder:.如果你想覆盖storyboard编码器,并且总是加载你的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;
}
Run Code Online (Sandbox Code Playgroud)


Sur*_*gch 13

别忘了

两个要点:

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

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

我的完整Swift回答是在这里.


归档时间:

查看次数:

60668 次

最近记录:

5 年,10 月 前