ios 使用 xib 创建可重用的 UIView(并从故事板加载)
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/21898190/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Creating a reusable UIView with xib (and loading from storyboard)
提问by Ken Chatfield
OK, there are dozens of posts on StackOverflow about this, but none are particularly clear on the solution. I'd like to create a custom UIView
with an accompanying xib file. The requirements are:
好吧,StackOverflow 上有很多关于这个的帖子,但没有一个特别清楚解决方案。我想UIView
使用随附的 xib 文件创建自定义。要求是:
- No separate
UIViewController
– a completely self-contained class - Outlets in the class to allow me to set/get properties of the view
- 没有单独的
UIViewController
——一个完全独立的类 - 类中的插座允许我设置/获取视图的属性
My current approach to doing this is:
我目前这样做的方法是:
Override
-(id)initWithFrame:
-(id)initWithFrame:(CGRect)frame { self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] objectAtIndex:0]; self.frame = frame; return self; }
Instantiate programmatically using
-(id)initWithFrame:
in my view controllerMyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)]; [self.view insertSubview:myCustomView atIndex:0];
覆盖
-(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];
This works fine (although never calling [super init]
and simply setting the object using the contents of the loaded nib seems a bit suspect – there is advice here to add a subview in this casewhich also works fine). However, I'd like to be able to instantiate the view from the storyboard also. So I can:
这工作正常(尽管从不调用[super init]
并简单地使用加载的笔尖的内容设置对象似乎有点可疑 -在这种情况下建议添加一个子视图也可以正常工作)。但是,我也希望能够从情节提要中实例化视图。所以我可以:
- Place a
UIView
on a parent view in the storyboard - Set its custom class to
MyCustomView
Override
-(id)initWithCoder:
– the code I've seen the most often fits a pattern such as the following:-(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]; }
- 将 a
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]; }
Of course, this doesn't work, as whether I use the approach above, or whether I instantiate programatically, both end up recursively calling -(id)initWithCoder:
upon entering -(void)initializeSubviews
and loading the nib from file.
当然,这不起作用,因为无论我是使用上面的方法,还是以编程方式实例化,最终都会递归调用从文件中-(id)initWithCoder:
输入-(void)initializeSubviews
和加载笔尖。
Several other SO questions deal with this such as here, here, hereand here. However, none of the answers given satisfactorily fixes the problem:
其他几个 SO 问题处理此问题,例如此处、此处、此处和此处。但是,给出的答案都不能令人满意地解决问题:
- A common suggestion seems to be to embed the entire class in a UIViewController, and do the nib loading there, but this seems suboptimal to me as it requires adding another file just as a wrapper
- 一个常见的建议似乎是将整个类嵌入到 UIViewController 中,并在那里加载笔尖,但这对我来说似乎并不理想,因为它需要添加另一个文件作为包装器
Could anyone give advice on how to resolve this problem, and get working outlets in a custom UIView
with minimum fuss/no thin controller wrapper? Or is there an alternative, cleaner way of doing things with minimum boilerplate code?
任何人都可以就如何解决这个问题提出建议,并UIView
以最少的麻烦/没有薄的控制器包装器定制工作插座吗?或者是否有另一种更简洁的方式以最少的样板代码做事?
采纳答案by Leo Natan
Your problem is calling loadNibNamed:
from (a descendant of) initWithCoder:
. loadNibNamed:
internally calls initWithCoder:
. If you want to override the storyboard coder, and always load your xib implementation, I suggest the following technique. Add a property to your view class, and in the xib file, set it to a predetermined value (in User Defined Runtime Attributes). Now, after calling [super initWithCoder:aDecoder];
check the value of the property. If it is the predetermined value, do not call [self initializeSubviews];
.
您的问题是loadNibNamed:
从 (的后代)调用initWithCoder:
。loadNibNamed:
内部调用initWithCoder:
。如果您想覆盖故事板编码器,并始终加载您的 xib 实现,我建议使用以下技术。向您的视图类添加一个属性,并在 xib 文件中将其设置为预定值(在用户定义的运行时属性中)。现在,在调用后[super initWithCoder:aDecoder];
检查属性的值。如果是预定值,则不调用[self initializeSubviews];
。
So, something like this:
所以,像这样:
-(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;
}
回答by Fattie
Note that this QA (like many) is really just of historic interest.
请注意,这个 QA(像许多人一样)实际上只是具有历史意义。
NowadaysFor years and years now in iOS everything's just a container view. Full tutorial here
如今,多年来,iOS 中的一切都只是一个容器视图。完整教程在这里
(Indeed Apple finally added Storyboard References, some time ago now, making it far easier.)
(事实上,Apple 终于在不久前添加了Storyboard References,让它变得更容易。)
Here's a typical storyboard with container views everywhere. Everything's a container view. It's just how you make apps.
这是一个典型的故事板,到处都是容器视图。一切都是容器视图。这就是您制作应用程序的方式。
(As a curiosity, KenC's answer shows exactly how, it used to be done to load an xib to a kind of wrapper view, since you can't really "assign to self".)
(出于好奇,KenC 的回答准确地显示了过去如何将 xib 加载到某种包装器视图,因为您不能真正“分配给自己”。)
回答by Ken Chatfield
I'm adding this as a separate post to update the situation with the release of Swift. The approach described by LeoNatan works perfectly in Objective-C. However, the stricter compile time checks prevent self
being assigned to when loading from the xib file in Swift.
我将此添加为单独的帖子,以随着 Swift 的发布更新情况。LeoNatan 描述的方法在Objective-C 中完美运行。但是,更严格的编译时检查会阻止self
在 Swift 中从 xib 文件加载时分配给。
As a result, there is no option but to add the view loaded from the xib file as a subview of the custom UIView subclass, rather than replacing self entirely. This is analogous to the second approach outlined in the original question. A rough outline of a class in Swift using this approach is as follows:
因此,别无选择,只能将从 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
}
}
The downside of this approach is the introduction of an additional redundant layer in the view hierarchy which does not exist when using the approach outlined by LeoNatan in Objective-C. However, this could be taken as a necessary evil and a product of the fundamental way things are designed in Xcode (it still seems crazy to me that it is so difficult to link a custom UIView class with a UI layout in a way that works consistently over both storyboards and from code) – replacing self
wholesale in the initializer before never seemed like a particularly interpretable way of doing things, although having essentially two view classes per view doesn't seem so great either.
这种方法的缺点是在视图层次结构中引入了一个额外的冗余层,当使用 Objective-C 中 LeoNatan 概述的方法时,该层不存在。然而,这可以被视为一种必要的邪恶,并且是 Xcode 中事物设计的基本方式的产物(对我来说仍然很疯狂,以一致的方式将自定义 UIView 类与 UI 布局链接起来是如此困难在故事板和代码中)——self
之前在初始化器中替换批发似乎从来都不是一种特别可解释的做事方式,尽管每个视图本质上有两个视图类似乎也不是那么好。
Nonetheless, one happy result of this approach is that we no longer need to set the view's custom class to our class file in interface builder to ensure correct behaviour when assigning to self
, and so the recursive call to init(coder aDecoder: NSCoder)
when issuing loadNibNamed()
is broken (by not setting the custom class in the xib file, the init(coder aDecoder: NSCoder)
of plain vanilla UIView rather than our custom version will be called instead).
尽管如此,这种方法的一个令人高兴的结果是,我们不再需要在界面构建器中将视图的自定义类设置为我们的类文件以确保分配给时的正确行为self
,因此init(coder aDecoder: NSCoder)
发出时的递归调用loadNibNamed()
被破坏(通过不设置xib 文件中的自定义类,将调用init(coder aDecoder: NSCoder)
普通的香草 UIView 而不是我们的自定义版本)。
Even though we cannot make class customizations to the view stored in the xib directly, we are still able to link the view to our 'parent' UIView subclass using outlets/actions etc. after setting the file owner of the view to our custom class:
即使我们不能直接对存储在 xib 中的视图进行类自定义,我们仍然可以在将视图的文件所有者设置为我们的自定义类后,使用 outlet/actions 等将视图链接到我们的“父”UIView 子类:
A video demonstrating the implementation of such a view class step by step using this approach can be found in the following video.
回答by rintaro
STEP1. Replacing self
from Storyboard
第1步。self
从故事板替换
Replacing self
in initWithCoder:
method will fail with following error.
更换self
的initWithCoder:
方法将失败,下面的错误。
'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
Instead, you can replace decoded object with awakeAfterUsingCoder:
(not awakeFromNib
). like:
相反,您可以用awakeAfterUsingCoder:
(not awakeFromNib
)替换解码的对象。喜欢:
@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
@end
STEP2. Preventing recursive call
第2步。防止递归调用
Of course, this also causes recursive call problem. (storyboard decoding -> awakeAfterUsingCoder:
-> loadNibNamed:
-> awakeAfterUsingCoder:
-> loadNibNamed:
-> ...)
So you have to check current awakeAfterUsingCoder:
is called in Storyboard decoding process or XIB decoding process.
You have several ways to do that:
当然,这也会造成递归调用问题。(故事板解码 -> awakeAfterUsingCoder:
-> loadNibNamed:
-> awakeAfterUsingCoder:
-> loadNibNamed:
-> ...)
所以你必须检查当前awakeAfterUsingCoder:
在故事板解码过程或 XIB 解码过程中被调用。您有几种方法可以做到这一点:
a) Use private @property
which is set in NIB only.
a) 使用@property
仅在 NIB 中设置的私有。
@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end
and set "User Defined Runtime Attributes" only in 'MyCustomView.xib'.
并仅在“MyCustomView.xib”中设置“用户定义的运行时属性”。
Pros:
优点:
- None
- 没有任何
Cons:
缺点:
- Simply does not work:
setXib:
will be called AFTERawakeAfterUsingCoder:
- 根本不起作用:
setXib:
将被调用后awakeAfterUsingCoder:
b) Check if self
has any subviews
b) 检查是否self
有任何子视图
Normally, you have subviews in the xib, but not in the storyboard.
通常,您在 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];
}
}
Pros:
优点:
- No trick in Interface Builder.
- Interface Builder 中没有技巧。
Cons:
缺点:
- You cannot have subviews in your Storyboard.
- 故事板中不能有子视图。
c) Set a static flag during loadNibNamed:
call
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;
}
}
Pros:
优点:
- Simple
- No trick in Interface Builder.
- 简单的
- Interface Builder 中没有技巧。
Cons:
缺点:
- Not safe: static shared flag is dangerous
- 不安全:静态共享标志很危险
d) Use private subclass in XIB
d) 在 XIB 中使用私有子类
For example, declare _NIB_MyCustomView
as a subclass of MyCustomView
.
And, use _NIB_MyCustomView
instead of MyCustomView
in your XIB only.
例如,声明_NIB_MyCustomView
为MyCustomView
. 并且,仅在您的 XIB 中使用_NIB_MyCustomView
而不是MyCustomView
。
MyCustomView.h:
我的自定义视图.h:
@interface MyCustomView : UIView
@end
MyCustomView.m:
我的自定义视图.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
Pros:
优点:
- No explicit
if
inMyCustomView
- 没有明确
if
的MyCustomView
Cons:
缺点:
- Prefixing
_NIB_
trick in xib Interface Builder - relatively more codes
_NIB_
xib Interface Builder 中的前缀技巧- 代码比较多
e) Use subclass as placeholder in Storyboard
e) 在 Storyboard 中使用子类作为占位符
Similar to d)
but use subclass in Storyboard, original class in XIB.
类似于d)
但在 Storyboard 中使用子类,在 XIB 中使用原始类。
Here, we declare MyCustomViewProto
as a subclass of MyCustomView
.
在这里,我们声明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
Pros:
优点:
- Very safe
- Clean; No extra code in
MyCustomView
. - No explicit
if
check same asd)
- 非常安全
- 干净的; 中没有额外的代码
MyCustomView
。 - 没有明确的
if
检查相同d)
Cons:
缺点:
- Need to use subclass in storyboard.
- 需要在故事板中使用子类。
I think e)
is the safest and cleanest strategy. So we adopt that here.
我认为e)
是最安全和最干净的策略。所以我们在这里采用。
STEP3. Copy properties
第3步。复制属性
After loadNibNamed:
in 'awakeAfterUsingCoder:', You have to copy several properties from self
which is decoded instance f the Storyboard. frame
and autolayout/autoresize properties are especially important.
之后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;
}
FINAL SOLUTION
最终解决方案
As you can see, this is a bit of boilerplate code. We can implement them as 'category'.
Here, I extend commonly used UIView+loadFromNib
code.
如您所见,这是一些样板代码。我们可以将它们实现为“类别”。在这里,我扩展了常用的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];
}
Using this, you can declare MyCustomViewProto
like:
使用它,您可以声明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:
西博:
Storyboard:
故事板:
Result:
结果:
回答by Suragch
Don't forget
不要忘记
Two important points:
两个重要的点:
- Set the File's Owner of the .xib to class name of your custom view.
- Don'tset the custom class name in IB for the .xib's root view.
- 将 .xib 的文件所有者设置为自定义视图的类名。
- 不要在 IB 中为 .xib 的根视图设置自定义类名。
I came to this Q&A page several times while learning to make reusable views. Forgetting the above points made me waste a lot of time trying to find out what was causing infinite recursion to happen. These points are mentioned in other answers here and elsewhere, but I just want to reemphasize them here.
在学习制作可重用视图的过程中,我多次来到这个问答页面。忘记以上几点让我浪费了很多时间试图找出导致无限递归发生的原因。这些要点在这里和其他地方的其他答案中都有提到,但我只想在这里再次强调它们。
My full Swift answer with steps is here.
我完整的 Swift 步骤答案在这里。
回答by ingaham
There is a solution which is much more cleaner than the solutions above: https://www.youtube.com/watch?v=xP7YvdlnHfA
有一个比上述解决方案更干净的解决方案:https: //www.youtube.com/watch?v=xP7YvdlnHfA
No Runtime properties, no recursive call problem at all. I tried it and it worked like a charm using from storyboard and from XIB with IBOutlet properties (iOS8.1, XCode6).
没有运行时属性,根本没有递归调用问题。我尝试了它,它使用故事板和带有 IBOutlet 属性(iOS8.1,XCode6)的 XIB 就像一个魅力。
Good luck for coding!
祝你编码好运!