好的,StackOverflow上有很多关于此的帖子,但没有一个在解决方案上特别清楚。我想创建一个带有随附xib文件的自定义UIView
。要求是:
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];
}
当然,这不起作用,因为无论我使用上述方法,还是以编程方式实例化,都会在输入-(id)initWithCoder:
并从文件加载nib时以递归方式调用-(void)initializeSubviews
。
其他几个SO问题涉及此问题,例如here,here,here和here。但是,没有一个答案能够令人满意地解决问题:
有人可以提供有关如何解决此问题的建议,并在自定义UIView
中使用最少的文件/没有瘦控制器包装来获得工作渠道吗?或者是否有另一种更清洁的方法来处理最少的样板代码?
答案 0 :(得分:26)
请注意,此质量保证(与许多人一样)确实具有历史意义。
现在多年来,在iOS中,所有东西都只是一个容器视图。 Full tutorial here
(事实上,苹果最近刚刚添加了Storyboard References,现在更容易了。)
这是一个典型的故事板,到处都有容器视图。一切都是容器视图。这就是你制作应用程序的方式。
(作为一个好奇心,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等:
可以找到使用此方法逐步演示此类视图类的视频in the following video。
答案 2 :(得分:16)
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
当然,这也会导致递归调用问题。 (故事板解码 - &gt; awakeAfterUsingCoder:
- &gt; loadNibNamed:
- &gt; awakeAfterUsingCoder:
- &gt; loadNibNamed:
- &gt; ...)
因此,您必须检查故事板解码过程或XIB解码过程中调用的当前awakeAfterUsingCoder:
。
您有几种方法可以做到这一点:
@property
。@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end
并仅在“MyCustomView.xib”中设置“用户定义的运行时属性”。
优点:
缺点:
setXib:
将被称为 AFTER awakeAfterUsingCoder:
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];
}
}
优点:
缺点:
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;
}
}
优点:
缺点:
例如,将_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
缺点:
_NIB_
技巧与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)
是最安全,最干净的策略。所以我们在这里采用它。
在'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:
故事板:
结果:
答案 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)
两个要点:
答案 5 :(得分:2)
有一种解决方案比上述解决方案更清洁: https://www.youtube.com/watch?v=xP7YvdlnHfA
没有运行时属性,根本没有递归调用问题。 我尝试了它,它就像一个使用故事板和来自XIB的魅力和IBOutlet属性(iOS8.1,XCode6)。
祝你好运!