好的,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 = 框架;回归自我;以编程方式使用 -(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 Override -(id)initWithCoder: - 我见过的最常符合以下模式的代码:-(id)initWithCoder:(NSCoder * )aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self initializeSubviews]; } 返回自我; } -(id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self initializeSubviews]; } 返回自我; } -(void)initializeSubviews { typeof(view) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] objectAtIndex:0]; [自我添加子视图:视图]; }
当然,这不起作用,因为无论我使用上述方法,还是以编程方式进行实例化,两者最终都会在输入 -(void)initializeSubviews
并从文件加载 nib 时递归调用 -(id)initWithCoder:
。
其他几个 SO 问题处理此问题,例如 here、here、here 和 here。但是,给出的答案都不能令人满意地解决问题:
一个常见的建议似乎是将整个类嵌入到 UIViewController 中,并在那里进行 nib 加载,但这对我来说似乎不是最理想的,因为它需要添加另一个文件作为包装器
任何人都可以就如何解决此问题提供建议,并在自定义 UIView
中获得工作插座,而无需大惊小怪/没有瘦控制器包装器?或者是否有另一种更简洁的方式来使用最少的样板代码?
请注意,这个 QA(像许多人一样)实际上只是具有历史意义。
如今 多年来,iOS 中的一切都只是一个容器视图。 Full tutorial here
(事实上,Apple 终于在不久前添加了 Storyboard References,使其变得更加容易。)
这是一个典型的故事板,随处可见容器视图。一切都是容器视图。这就是您制作应用程序的方式。
https://i.stack.imgur.com/CdGH7.png
(出于好奇,KenC 的回答准确地显示了过去如何将 xib 加载到一种包装器视图中,因为您不能真正“分配给自己”。)
我将此作为单独的帖子添加,以更新 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)
时递归调用 init(coder aDecoder: NSCoder)
3} 被破坏(通过不在 xib 文件中设置自定义类,将调用普通 UIView 的 init(coder aDecoder: NSCoder)
而不是我们的自定义版本)。
即使我们不能直接对存储在 xib 中的视图进行类自定义,在将视图的文件所有者设置为我们的自定义类之后,我们仍然可以使用 outlets/actions 等将视图链接到我们的“父”UIView 子类:
https://i.stack.imgur.com/9p0ib.png
可以找到演示使用此方法逐步实现此类视图类的视频in the following video。
UIView
的建议。我同意苹果从来没有让这件事变得简单是疯狂的,现在这几乎是不可能的。容器并不总是答案。
步骤1。从情节提要中替换自我
在 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:
解码过程或XIB解码过程。你有几种方法可以做到这一点:
a) 使用仅在 NIB 中设置的私有 @property。
@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end
并仅在“MyCustomView.xib”中设置“用户定义的运行时属性”。
优点:
没有任何
缺点:
根本不起作用: setXib: 将在 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) 在情节提要中使用子类作为占位符
与 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
复制几个属性,这些属性是 Storyboard 的解码实例。 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
西布:
https://i.stack.imgur.com/2dpTM.png
故事板:
https://i.stack.imgur.com/8FGiy.png
结果:
https://i.stack.imgur.com/JOl5G.png
您的问题是从 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;
}
有一个比上述解决方案更清洁的解决方案:https://www.youtube.com/watch?v=xP7YvdlnHfA
没有运行时属性,完全没有递归调用问题。我尝试了它,它使用来自故事板和来自具有 IBOutlet 属性(iOS8.1,XCode6)的 XIB 就像一个魅力。
祝你编码好运!