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

时间:2014-02-20 04:33:37

标签: ios objective-c uiview storyboard nib

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

  • 没有单独的UIViewController - 一个完全独立的课程
  • 类中的Outlets允许我设置/获取视图的属性

我目前的做法是:

  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];
    
  3. 这样可以正常工作(虽然从不调用[super init],只是使用加载的nib的内容设置对象似乎有点怀疑 - 这里有add a subview in this case的建议也可以正常工作。但是,我希望能够从故事板中实例化视图。所以我可以:

    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];
      }
      
    4. 当然,这不起作用,因为无论我使用上述方法,还是以编程方式实例化,都会在输入-(id)initWithCoder:并从文件加载nib时以递归方式调用-(void)initializeSubviews

      其他几个SO问题涉及此问题,例如herehereherehere。但是,没有一个答案能够令人满意地解决问题:

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

      有人可以提供有关如何解决此问题的建议,并在自定义UIView中使用最少的文件/没有瘦控制器包装来获得工作渠道吗?或者是否有另一种更清洁的方法来处理最少的样板代码?

6 个答案:

答案 0 :(得分:26)

请注意,此质量保证(与许多人一样)确实具有历史意义。

现在多年来,在iOS中,所有东西都只是一个容器视图。 Full tutorial here

(事实上,苹果最近刚刚添加了Storyboard References,现在更容易了。)

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

enter image description here

(作为一个好奇心,KenC的答案显示了如何将xib加载到一种包装器视图中,因为你无法真正"分配给自己" 34。)

答案 1 :(得分:22)

我将此作为单独的帖子添加,以通过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中存储的视图进行类自定义,我们仍然可以将视图链接到我们的父母&#39;在将视图的文件所有者设置为我们的自定义类之后,UIView子类使用outlet / actions等:

Setting the file owner property of the custom view

可以找到使用此方法逐步演示此类视图类的视频in the following video

答案 2 :(得分:16)

STEP1。从故事板

替换self

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

'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

STEP2。防止递归调用

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

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

优点:

  • if
  • 中没有明确的MyCustomView

缺点:

  • 在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)是最安全,最干净的策略。所以我们在这里采用它。

STEP3。复制属性

在'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,如:

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

故事板:

Storyboard

结果:

enter image description here

答案 3 :(得分: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;
}

答案 4 :(得分:13)

别忘了

两个要点:

  1. 将文件的.xib所有者设置为自定义视图的类名。
  2. 不要在IB中为.xib的根视图设置自定义类名。
  3. 在学习制作可重复使用的视图时,我多次来到此Q&amp; A页面。忘记上述几点让我浪费了大量时间试图找出导致无限递归发生的原因。这里的其他答案和elsewhere中都提到了这些要点,但我只想在此重新强调它们。

    我的完整Swift回答是here

答案 5 :(得分:2)

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

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

祝你好运!