iOS开发xib 原理、嵌套、可视化、继承

释放双眼,带上耳机,听听看~!

xib嵌套

封装了一个view,它自带有xib。想让它能在其他xib、SB中使用

1.绑定xib的File Owner
(此xib的所有者,管理这个xib的逻辑、交互等,类似于ViewControllerview的管理)

绑定File Owner.png

(这里你可能会疑问为什么不直接绑定view为此类,如下图。这种做法也可以,但只适用于用代码创建的场景,如自定义带xib的UITableViewCell,每个cell是由代码创建的。不能内嵌到其他xib、SB中使用,后面有详细解释)

绑定view.png

2.在view的.m文件中实现下面代码就行了

- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self loadViewFromXib];
}
return self;
}
- (void)loadViewFromXib
{
// 取xib中view的方法一:
UIView *contentView = [[NSBundle bundleForClass:[self class]] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil].firstObject;
// 取xib中view的方法二:
// UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:[NSBundle bundleForClass:[self class]]];
// UIView *contentView = [nib instantiateWithOwner:self options:nil].firstObject;
/*
这里取view有两种方法,方法一一步完成,方法二拆成了两步。
方法二的好处在于我们可以保留取出来的nib,以后直接用nib生成view,不会像方法一总是会重复去取nib。
方法二常见于带xib的TableViewCell,因为cell会大量重用,所以方法二取一次nib保存后,以后要用cell只用执行它的第二句代码来生成view,更加高效
一般情况下,两个没什么区别,用哪个看个人喜好
*/
contentView.frame = self.bounds;
contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth| UIViewAutoresizingFlexibleHeight;
[self addSubview:contentView];
}

 

xib可视化

可以让封装好的xib,在其他xib或者SB中实时预览

只需在上面代码基础上加入以下三步:

  1. view的.h中声明 IB_DESIGNABLE。(声明此视图需要支持预览)
  2. 要写 initWithFrame:方法。(xcode预览的工具,显示view时会创建这个View,此时走的view的这个初始化方法)
  3. [NSBundle bundleForClass:[self class]]]取对对应的Bundle。(可能实时运行的预览工具,取资源文件的位置和app实际运行时不一样)

完整代码如下:(以后可直接复制使用)

.h
IB_DESIGNABLE
@interface CustomView : UIView
@end
.m
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self loadViewFromXib];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self loadViewFromXib];
}
return self;
}
- (void)loadViewFromXib
{
// 对于Bundle不太了解,但这里要用[NSBundle bundleForClass:[self class]]]取对对应的Bundle,而不是用mainBundle或者nil
UIView *contentView = [[NSBundle bundleForClass:[self class]] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil].firstObject;
contentView.frame = self.bounds;
contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth| UIViewAutoresizingFlexibleHeight;
[self addSubview:contentView];
}

// 顺便一提属性使用IBInspectable可在xib的右侧的属性(Attributes inspector)标签栏中看到
@property (nonatomic, assign) IBInspectable BOOL semicircle;///< 始终一般高度的圆角

// 使用IBOutlet可在连接(Connections inspector)标签栏中看到,一般用来连线到File Owner
@property (nonatomic, weak) IBOutlet id customDelegate;

如果一直没显示出来:
1.类名不能绑定在view上,要绑在File Owner上,否则会死循环crash
2.使用下面的【view Debugging】方法

view Debugging

当为view增加 IB_DESIGNABLE时,可能经常出现 error: IB Designables: Failed to render and update auto layout status for UIView (i5M-Pr-FkT): The agent crashed的情况

此时有两种调试方法:
1.在xib或者sb中,选择自定义视图,然后选择 EditorDebug Selected Views。这会重新对这view运行 IBDesignableAgentCocoaTouch,可以进入视图的断点进行调试

2.在 ~/Library/Logs/DiagnosticReports目录中有命名为 IBDesignablesAgentCocoaTouch_*.crash的崩溃日志,记录了堆栈信息

预览专用方法

预览专用方法,程序实际运行时不调用。
用于在其他xib、SB中预览你customView时,配置一些参数的默认值,以便能完整的预览到customView效果

/// 预览时调用,程序实际运行时不调用
- (void)prepareForInterfaceBuilder
{
self.arrTitle = @[@"预览", @"预览文案", @"在程序运行时不会执行,预览文案预览文案", @"预览文案,在程", @"预览文案,在程序运行时不会执行预览文案,在程序运行时不会执行"];
}

 

xib原理

为什么不先讲原理呢?怕吓跑你们?

xib本身是xml格式的资源文件,上面记录了什么控件应该摆在哪里,控件本身设置了哪些属性等,和你的图片、音视频差不多,所以本身是无法”直接”继承的。
是的,我们可以”间接”达到继承的效果,后面会说。

名词释义:
xib:xml格式的文件,记录控件、属性及其之间位置关系。(我们在xcode中直接看到的)
nib:xib被加密、序列化后的二进制文件(data)。(app打包后,将包拆开后可以看到.nib后缀名的文件)
序列化:归档(将xib加密成nib的过程)
反序列化:解档(将nib解密,提取到内存中,成为我们能直接使用的类的过程)
顶级对象:xib面板左侧对象边栏中的最外层的对象,如最外层的view。instantiateWithOwner:和loadNibNamed:owner:方法返回的数组中,保存的都是顶级对象(原谅我表述不清,自己的理解不深)
xib加载流程:
  1. 提取nib文件到内存中
    Bundle中取出nib文件,为二进制文件,加入到内存中
  2. 对原xib中所有view对象进行解档
    a) 从内存中的二进制数据,取出原xib中的各view对应那部分data
    b) 通过调用initWithCoder:初始化方法,创建原xib中的所有view,将上面的那部分data作为入参传入
    c) 这里是每个view进行反序列化,将二进制文件转为实际的类。实际上不需要我们亲自来反序列化,在initWithCoder:方法中调用[super initWithCoder:coder]即可,系统的根类中已经默认做好了
    d) 注意❗️:每个view(包括顶级对象view)在xib中绑定的什么类,就会创建这个类。例如一个View没有绑定类名,默认系统的UIView类,那么实际就是调用的[UIView initWithCoder:aData],这个view解档完成后就是UIView的实例;如果一个View绑定类名为CustomView,那么实际就是调用的[CustomView initWithCoder:aData],然后就进入到CustomView类中的initWithCoder:方法了,这个view解档完成后就是CustomView的实例。
    e) 注意❗️:在initWithCoder:方法中,不能使用xib、SB连线出来的属性,此时连线的属性都为nil,因为现在还没开始关联属性
  3. 关联属性和方法
    对连线到自己类File OwnerObject中的属性进行弱引用关联,并关联事件。然后这三个地方就可以使用连线过来的属性和响应连线的过来方法了。(下面会讲怎么连线到这三个地方)
  4. 解档完成
    原xib中各个view解档完成后,调用各自的awakeFromNib方法,告诉你xib已经完全ok,可以直接使用了。现在你可以在awakeFromNib方法中,使用xib、SB连线出来的属性了。

拓展点:xib、SB可以连线到哪些地方

1)连线到自己类
自己的东西(子视图、手势、Object类等),能通过连线,被自己使用,这没毛病。

连线到自己类.png

2)连线到File Owner
自己的所有者使用自己东西,也挺合理。
File Owner一般作为管理xib的地方,像ViewController包含此xib的视图

连线到File Owner.png

3)连线到Object
Object是任意类,一般是继承于NSObject的逻辑类,用于处理视图中的逻辑部分,这就很厉害了。
Object其实分为ObjectExternal Object两种,前者xib、SB会直接在解档的时候创建一个,后者则是需要在xib初始化时,传入的已存在的实例,不由xib创建。

使用Object很简单,直接拖入一个Object,然后绑定类名,就可以关联属性及事件了
这里的CustonViewManager类,可以当做专门处理CustonView中逻辑的类

使用Object.png

使用External Object稍微复杂点,拖入一个External Object后,需要先绑定类名(class)及标识符(Identifier)
这里的TestViewController是CustonView的File Owner,TestViewControllerManager类可以当做是处理TestViewController中逻辑的类

绑定类名及标识符.png

然后在xib初始化时,传入External Object的实例。

传入External Object的实例.png

最后就可以在TestViewControllerManager中也使用CustonView中的东西啦

External Object中关联属性及事件.png

所以,例如xib、SB中有一个button时,我们如果将这个button连线到这三个地方,那么在xib解档后,这三个地方都能修改这个button的属性。
同理,如果将这个button的点击事件连线到到这三个地方,那么这三个地方都能响应button的点击事件(三个地方点击事件的执行顺序暂时还没测试过,不过我觉得这里的作用是分开执行不相关联的逻辑,不能太依靠事件的执行顺序)。
这样就能给代码瘦身,封装出更优雅的View。不过也因为太灵活,所以使用时需要谨防矫枉过正

拓展点:将类名绑File Owner和绑顶级对象view的区别

这点文章开头xib加载流程中都有提到过。

CustonView.xib为例:
你如果将顶级对象view的类名绑定为CustonView,那么在xib解档过程中,对顶级对象view解档后,返回的就是CustonView这个实例。也就是loadNibNamed: owner:self options:这个方法返回的数组中,你拿到的就是custonView的实例

你如果将File Owner的类名绑定为CustonView,那么CustonView类CustonView.xib的管理者,CustonView.xib只负责给CustonView类提供打包好的view。也就是loadNibNamed: owner:self options:这个方法返回的数组中,你拿到的就是UIView的实例

多数情况下,我们都是绑定File Owner的类名,为什么呢?举个栗子?

我封装了一个RedButton的控件,它自带有RedButton.xib,里面是一些图片、文案等,且在RedButton.xib中的绑定顶级对象view为RedButton。这时我在mainViewController.xib中需要用到封装好的RedButton,我就拖一个button到xib,然后将类名改为RedButton。

接下来我们在脑海中模拟一下mainViewController.xib解档的整个过程:

  1. mainViewController.xib开始解档,它要通过二进制数据(nib)创建原xib中所有的view,所以调用每个view的initWithCoder:方法
  2. 接着RedButton被调用initWithCoder:方法,此时initWithCoder:时没有写任何代码的,程序继续走,RedButton实例生成成功。

但是最后我们发现,mainViewController.xib中RedButton这部分是一片空白。这时我们回想一下就会发现,mainViewController.xib只是会通过initWithCoder:方法创建了RedButton这个类,单纯的创建这个类,并不会加载RedButton.xib这个文件,所以RedButton缺失了视图元素。

那么我们将代码改下,在initWithCoder:中加载RedButton.xib,将其顶级对象view作为自己子视图:

- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self loadViewFromXib];
}
return self;
}
- (void)loadViewFromXib
{
UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:[NSBundle bundleForClass:[self class]]];
UIView *contentView = [nib instantiateWithOwner:self options:nil].firstObject;
contentView.frame = self.bounds;
[self addSubview:contentView];
}

重新运行程序后,会发现程序crash了,堆栈信息中显示陷入了死循环。我们重新理一下现在的调用顺序:

  1. mainViewController.xib开始解档,它要通过二进制数据(nib)创建原xib中所有的view,所以调用每个view的initWithCoder:方法
  2. 接着RedButton要开始解档,通过调用[RedButton initWithCoder:aData],此时initWithCoder:中要加载RedButton.xib
  3. RedButton.xib开始解档,调用每个view的initWithCoder:方法,包括顶级对象view。
  4. 因为顶级对象view的类名绑定的是RedButton,顶级对象view开始解档,调用[RedButton initWithCoder:aData],果然进入了死循环

这要怎么解决呢?将xib中绑顶级对象view的类名改为绑File Owner的类名就行了。
这说明封装的带xib的view,如果要支持嵌套在其他xib中,则不能绑定顶级对象view的类名,只能通过绑定File Owner的类名,将RedButton.xib作为单纯的视图元素加到自己的view上。
这也就是为什么绑File Owner的类名的做法更常见。

那不能将封装的带xib的view,绑顶级对象view的类名么?可以的,上面代码不变,xib绑顶级对象view的类名不变。不过只能用于代码创建xib的场景,如VC中,用代码创建自定义view(RedButon *btn = [self loadNibNamed:@"RedButton" owner:self options:].firstObject),又例如在UITableView的代理中,创建带xib的Cell。

结论:在xib中
绑顶级对象view的类名:只能用于代码创建此类
绑File Owner的类名:代码创建此类、嵌套到其他xib中都可以(常用)

xib继承

以聊天页面为例(如QQ),里面的每条消息都是一行cell,大致有这几种cell:文字消息行、图片消息行、视频消息行、红包消息行。这些cell都有共同的部分,就是左右的人物头像,以及气泡背景。
一般第一反应就是做成继承的类型,父类cell中有共同部分,子类cell中只有各自差异化的东西,如父类cell有头像、气泡,文字消息子cell中只有文字,图片消息子cell中只有图片。

具体怎么做呢?别急,还需要了解两个东西:
1.子类xib中的元素,可以连线到父类中。不管是绑顶级对象view还是绑File Owner
这里举例不当,没必要同时连线到父类和子类中,因为子类会继承父类中的属性,希望别被我误导了

子类xib连线到父类.png

2.我一般使用UITableView时会开始就注册cell,这样就不用在- tableView: cellForRowAtIndexPath:中判断cell是不是存在,因为注册了cell后,然后从重用队列中取cell时,如果取不到系统会自动创建一个新的cell返回给你。
下面是重点:

注册cell有两种方式:
1.使用[_tableView registerNib: forCellReuseIdentifier:]方法: 通过nib创建cell
用于创建自带xib的cell,也就是cell.xib中,必须绑定顶级对象的类名为当前cell。
创建时,会走cell的initWithCoder:方法,不会走别的初始化方法

2.使用[_tableView registerClass: forCellReuseIdentifier:]方法: 通过类名来创建cell
一般用于创建没有xib的cell。如果cell有xib,也要使用这种注册方法怎么办,在cell.xib中,绑定File Owner为当前cell。
创建时,会走cell的initWithStyle: reuseIdentifier:方法,不会走别的初始化方法

现在我们就可以试着想怎么实现上面说的聊天信息cell的继承:
前提:代码上,子类cell都继承父类cell,如文字消息cell图片消息cell视频消息cll等,都继承自父类消息cell
那么,父类cell是否拥有xib、父类或子类cell的xib中的内容是完整的还是一部分、这些xib是通过registerNib的方式注册还是通过registerClass的方式注册,这些组合起来就有很多可能。

方案一:

Demo: 方案一Demo

父类和子类都有各自的xib,父类的xib是公共部分,子类的xib是差异部分。(xib全部绑定的是File Owner)
子类通过registerClass注册, 在初始化时,先加载父类xib中内容,addSubView到cell上。然后加载自己xib,addSubView到指定区域(气泡)中。

 

方案一xib.png

先注册cell (一定要使用registerClass的方式注册,不能是registerNib

// 首先使用registerClass的方式注册子类cell
[_tableView registerClass:[TextChatCell class] forCellReuseIdentifier:NSStringFromClass([TextChatCell class])];
[_tableView registerClass:[ImageChatCell class] forCellReuseIdentifier:NSStringFromClass([ImageChatCell class])];
// 然后就可以用重用获取cell了
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *dict = _arrData[indexPath.row];
NSString *type = dict[@"type"];
if ([type isEqualToString:@"text"]) {
// text
TextChatCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([TextChatCell class]) forIndexPath:indexPath];
return cell;
} else {
// image
ImageChatCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([ImageChatCell class]) forIndexPath:indexPath];
return cell;
}
}

父类cell实现

@implementation BaseChatCell
#pragma mark - Life Circle
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self loadViewFromXib];
}
return self;
}
/// 加载父类xib
- (void)loadViewFromXib
{
UINib *nib = [UINib nibWithNibName:NSStringFromClass([BaseChatCell class]) bundle:[NSBundle mainBundle]];
UIView *contentView = [nib instantiateWithOwner:self options:nil].firstObject;
[self.contentView addSubview:contentView];
// 添加约束,让内容充满cell
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[contentView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(contentView)]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[contentView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(contentView)]];
}
@end

子类cell实现(这里以文字消息cell为例,其他子类cell中代码一模一样)

@implementation TextChatCell
#pragma mark - Life Circle
- (void)loadViewFromXib
{
// 加载父类xib中内容
[super loadViewFromXib];
// 加载当前类xib中内容
[super loadChildViewFromXib];
}
/// 加载子类xib
- (void)loadChildViewFromXib
{
// 加载子类xib,将差异化的视图元素,加载到父视图指定区域内
UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:[NSBundle mainBundle]];
UIView *contentView = [nib instantiateWithOwner:self options:nil].firstObject;
[self.viewContent addSubview:contentView];
// 添加约束,让内容充满cell
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[contentView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(contentView)]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[contentView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(contentView)]];
// 做一些处理,其实这些也可以在xib直接设置好
contentView.backgroundColor = [UIColor clearColor];
self.viewContent.backgroundColor = [UIColor clearColor];
}
@end

 

因为每个子类中的loadChildViewFromXib方法实现都是一样的,所以我们可以把这个方法提取出来,放到父类中,子类直接调用,免去了每次都在子类中实现一遍

@implementation BaseChatCell
#pragma mark - Life Circle
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self loadViewFromXib];
}
return self;
}
/// 加载父类xib
- (void)loadViewFromXib
{
UINib *nib = [UINib nibWithNibName:NSStringFromClass([BaseChatCell class]) bundle:[NSBundle mainBundle]];
UIView *contentView = [nib instantiateWithOwner:self options:nil].firstObject;
[self.contentView addSubview:contentView];
// 添加约束,让内容充满cell
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[contentView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(contentView)]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[contentView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(contentView)]];
}
/// 加载子类xib
- (void)loadChildViewFromXib
{
// 加载子类xib,将差异化的视图元素,加载到父视图指定区域内
UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:[NSBundle mainBundle]];
UIView *contentView = [nib instantiateWithOwner:self options:nil].firstObject;
[self.viewContent addSubview:contentView];
// 添加约束,让内容充满cell
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[contentView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(contentView)]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[contentView]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(contentView)]];
// 做一些处理,其实这些也可以在xib直接设置好
contentView.backgroundColor = [UIColor clearColor];
self.viewContent.backgroundColor = [UIColor clearColor];
}
@end
@implementation TextChatCell
#pragma mark - Life Circle
- (void)loadViewFromXib
{
// 加载父类xib中内容
[super loadViewFromXib];
// 加载当前类xib中内容
[super loadChildViewFromXib];
}
@end

 

方案二:

父类没有xib,每个子类xib,都是完整的内容。(xib全部绑定的是顶级对象view)
子类通过registerNib注册cell。

方案二xib.png

先注册cell(这里注册方式与方案一相反,要用registerNib的方式注册,不能是registerClass

// 首先使用registerNib的方式注册子类cell
[_tableView registerNib:[UINib nibWithNibName:NSStringFromClass([CustomerServiceTextCell class]) bundle:nil] forCellReuseIdentifier:NSStringFromClass([CustomerServiceTextCell class])];
[_tableView registerNib:[UINib nibWithNibName:NSStringFromClass([CustomerServiceImageCell class]) bundle:nil] forCellReuseIdentifier:NSStringFromClass([CustomerServiceImageCell class])];
// 然后就可以用重用获取cell了
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomerServiceTextCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([CustomerServiceTextCell class]) forIndexPath:indexPath];
}

然后就没了?,方案二的没有代码上需要处理的,唯一要注意的点就是子类xib中共同的视图元素,都可以连线到父类中,然后就在父类的代码中处理公共的逻辑。

结论:
方案一:方便以后的拓展、易维护。虽然理解上可能比方案二稍稍麻烦点,但多用几次就好了,推荐。
方案二:很来很简单,适合刚接触xib的人,但强烈不推荐。你日后维护起来会很崩溃的,只要公共部分有改动,就需要去每个子类xib中修改,而且这里面一堆约束,很容易出错的。

当然还有其他组合出来的方案,但就我所知的情况中,和这两种都是大同小异,有兴趣的可以研究下其他的搭配。

人已赞赏
iOS文章

iOS弧形布局(圆形布局),可滑动

2020-4-27 15:01:49

iOS文章

iOS简单实现UITableVIew每组(section)圆角

2020-4-27 16:16:48

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索