xib的一些感悟
引言
这两天突然想起之前看到过有的项目中用到了object, 好奇心起,就顺便回溯下xib的使用吧
xib优缺点分析
1.有一定的学习成本
2.没有代码表达清晰
3.出错不易发现,无法调试,尤其是“连线”出了问题
4.文件易冲突,且难解决,不利于团队合作,尤其是在团队中用SB
5.执行效率没有代码高
6.有时不利于封装
7.灵活性不强,尤其界面灵活多变, 高度不定
8.使用富文本卡顿现象明显
优点
1.开发效率高
2.减少大量胶水代码
3.通过xib可以快速、高效的学习控件
4.适配性明显优于代码(auto layout、size classes)
5.可视化
6.在一定程度上修改方便
VC View 使用xib
1 创建方法
VC在创建的时候可以之间勾选创建 xib
image
View不能够直接勾选创建,需要command+N选择View创建一个xib,一般来说,我们需要创建一个相对应的类, 在右边栏第三个选项(show the identity inspector)下面的custom class-> class中填写你要与该xib绑定的UIView子类的名字
2 初始化方法
VC加载xib的方法
我们的父类在初始化的时候去自动帮我们找与之对应的xib文件,那么问题来了,父类怎么知道我有没有xib文件呢?是这样,父类会判断有没有和我们这个要初始化的VC相同名字的xib文件,如果有就会加载该xib文件,如果没有,父类就认为我们该VC没有xib文件,就会走正常的init方法。
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;
如果一旦我们的VC类的名字与对应的xib文件名字不同的时候,我们就必须调用这个初始化方法来创建VC实例了;
[[ViewController alloc] initWithNibName:@"xxx" bundle:[NSBundle mainBundle]]
xxx填写xib文件的名字, 名字不同也是可以的
2 view加载xib
TestView *tView = [[NSBundle mainBundle] loadNibNamed:@"TestView" owner:self options:nil].firstObject;
上述代码再次说明xib文件是资源文件,放在main bundle中,@"TestView"是xib文件的名称,后面两个参数暂时不用了解,就固定传self和nil就行,值得说的是,loadNibNamed: owner: options方法返回的是一个数组,而不直接是对象,这是考虑到了Mac开发会有多个对象返回的情况,在iOS开发中就只有一个,固定取第一个就好。
注:一般的UIView对象,代码初始化的时候都会调用initWithFrame:方法,但是用xib创建的UIView对象是不会调用此方法的,因为该对象的Frame在xib文件中就可以确定了。以xib的形式保存控件对象的过程其实叫做固化(archive),通过xib文件创建控件的过程叫做解固(unarchive),固化是iOS持久化的一种比较好的解决方案,以后有机会会说说iOS持久化的各种方式的优劣,这里不再深入,而与固化相关的初始化函数是:
- (instancetype)initWithCoder:(NSCoder *)aDecoder
所以,当以xib创建UIView对象的时候这个函数会调用,之前在initWithFrame:中要做的事情,可以放在initWithCoder:中,或者放在:
- (void)awakeFromNib
{
[super awakeFromNib];
//...
}
SB文件的使用
由于SB文件与VC一般是一对多的关系,所以我们不仅要知道即将创建的这个VC的实例对象是加载的哪个SB,而且还要知道加载的是该SB中的哪个具体的VC
SecVC *secVC = [[UIStoryboard storyboardWithName:@"Demo" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:@"SecVC"];
有的项目有多个sb进行跳转, 一般来说需要找到那个sb的初始入口那个VC
SecVC *secVC = [[UIStoryboard storyboardWithName:@"Demo" bundle:[NSBundle mainBundle]] instantiateInitialViewController];
冲突解决
xib、SB文件有两种查看方式,当文件冲突的时候普通的interface builder方式打开文件会失败。
此时右键该xib或SB文件,选择source code方式打开文件,之后解决冲突的思路与解决project.pbxproj文件冲突的方式是一样的,文件中全局搜索<<<<或>>>>定位到错误位置,根据实际情况,删除一个版本的代码,保留另一个版本的代码,直到文件中所有冲突都解决掉。
File Owner
Files Owner指这个xib文件的所属文件是谁,简单的说是xib文件和谁建立起交互,用户通与该xib呈现的页面进行交互的时候,谁来处理背后的逻辑。具体来讲xib文件能拖动“连线”到哪个源文件中去建立IBAction、IBOutlet、delegate、datasource等。
一般基于View创建的xib的Files Owner都指定为一个VC(但一般应用都会创建一个相对应的类,然后指向该类)。基于VC创建的xib,创建的时候系统就已经把该xib文件的Files Owner指向了该VC,一般这种情况就不对Files Owner做修改了。
1490498-5f79953535024ab1.jpgIBAction
建立IBAction连线的方法:
1.选中需要连线的对象,按住control键,拖动该控件到Files Owner类的@implementatio中松手,填写方法名即可。
建立IBAction连线的方法:
1.选中需要连线的对象,按住control键,拖动该控件到Files Owner类的@implementatio中松手,填写方法名即可。
1490498-56ce8578de3b63d8.gif
2.先在@implementatio中定义一个方法,在返回值中写IBAction,然后点击前面的空心圆,拖动到xib的对象上,两者就建立了“连线”的关系。 1490498-fe72df4448bcd619.gif
IBOutlet
也是“连线”的一种,用于标记属性或变量,此方法将Files Owner中的属性或字段,与xib中的某个对象通过“连线”建立起关系。
IBOutlet建立“连线”与查看“连线”的方法与IBAction相同。但要注意的是:如果拖动“线”到@interface里,就生成属性,如果拖动到@interface{}里或者拖动“线”到@implementatio中的{}里就生成变量。
这里简单说一下:在@implementation中{}里写变量(一般不这样干)和在@interface XXClass()(匿名Category)里的{}中写变量是一样的,都是匿名的。
不怎么用却实用的
基于View创建的xib,是可以“连线”到自己View所在类中的,如果给该xib设置了Files Owner的属性后,可以同时“连线”到Files Owner的类中。
我们可能会遇到这种情况:要封装一个View为一个单独的类,该View是可交互的,点击后,要发生变化,同时又要把交互的事件传递给VC,如果用代码的话,就要把事件从View类传递给VC类,而如果你封装的类用了xib,那么事情就简单多了,同时“连线”到View类和自己Files Owner类对应的VC中,在View类中处理UI的变化,在VC中处理逻辑,而不需要任何事件的传递,当用户交互的时候,这两根“连线”都会被回调。
SB使用
xib可以基于View、VC甚至自己独立的使用,而SB只能基于VC使用,为什么说比xib更加强大呢?主要是下面的两个原因:
1.SB支持segue
2.SB对cell的支持更加强大
1490498-c779b4a50255c30f.gif)]
我们在一个VC中选择要发生跳转的按钮,按control拖动到另一个VC上就会出现一个菜单,在菜单上你就可以选择跳转的方式push、present,这样不用写一行代码就能完成页面间的跳转,而两个VC之间的像纽扣一样用线连着两个VC的的东西就是segue,是一个UIStoryboardSegue对象,我们可以简单的理解成是完成页面跳转相关功能的一个类,是不是很简单?
segue虽然简单,如何传参?
我们知道用SB的segue来实现也页面跳转十分方便,但是如果要向跳转的页面传参该怎么办?
假设我们要点击ViewController这个VC里的一个按钮,跳转到SecVC这个VC中,把testTitle这个参数传过去。第一步还是要选中按钮,拖到SecVC里,然后选中这个segue,给它一个id值,确保用代码可以找到它,然后在ViewController中编码:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:@"sec"]) {
SecVC *secVC = (SecVC *)segue.destinationViewController;
secVC.testTite = @"hello world";
}
}
设置SB中segue的identifier,主要就是要在这里使用,因为多个button发生的多个页面跳转都是回调这个函数,所以要用identifier来区分是哪个segue,跳哪个页面,传哪些参数。
不确定的跳转如何使用segue?
一下场景经常出现:甲VC中有个button,点击后有可能跳转到乙页面,也可能跳转的丙页面,怎么解?
我们假设我们要点击ViewController这个VC里的一个按钮,有可能要跳到SecVC,有可能要跳thirdVC。
1.在SB中准备segue。
1490498-a890aa691ffb90b7.gif
注意我们这次创建的segue的方式并不是选中button按control拖到另一个VC中,而是选中VC,右键选中triggered segues里manual后面的+拖动到另一个VC里,这个很重要,而且还是要给segue一个id。
更灵活的segue
如果你熟练掌握了segue他其实可以做很多事情,其实segue并不仅仅能完成跳转,还有一类segue叫做relationship segue,之前是帮助大家理解,所以说的比较简单,其实我们选中一个NavigationController按control键拖动到另一个VC上的话也可以生成segue,我们选中root view controller,就将Nav的rootViewController属性设置成了这个VC,这个segue就是relationship segue,同样,拖动一个tabbarController也会出现viewControllers的relationship segue。
更好的使用Cell
用xib的时候,要想用cell,通常是建立一个cell子类,一个与之对应的.xib文件。用SB的时候要想用cell就简单的多了,因为cell是可以直接拖动到tableView里的,直接在tableView里管理cell设置属性,当然如果是复杂的cell还是要子类话对应的.h、.m的。
SB在使用cell分两种情况:1静态cell,2动态cell。
静态cell 1490498-240e6c7c16d2536a.jpg
注意静态cell一定要基于UITableViewController,否则会报错。
展示静态cell的tableView是不用调用自己必须实现的datasource协议的
你会很惊讶,为什么必须调用的datasource协议都不用实现?答案是静态,静态的cell,意义就在于你在xib中设置成什么样,他就展现什么样,不会再调用datasource向你要cell了,因为在SB文件中已经确定下来了。
在做一些“死”页面的时候SB的静态cell是很好的选择,静态cell也不是什么都不能做,静态cell里的button还是可以拖到@implementation中形成IBAction的,但是是无法生成IBOutlet属性或字段的。
即使你强行的给一个静态的cell指定了一个cell的类,也是无法向其内部拖入IBOutlet的。这就是静态cell的局限性,但是如果你要设置的数据不多还是可以考虑用静态cell,因为你可以通过给cell上的控件设tag来找到它从而赋值。
动态cell的使用
在上图中设置content属性为Dynamic Prototypes就可以了。使用动态cell的话就要在VC中实现那两个必须实现的datasource协议了,下面举个简单的例子:
1.给cell指定一个class
2.给cell设置id,重用机制使用,其他属性就不详细说明了,和其他控件的属性差不多。
添加tableHeaderView和tableFooterView
是的,没有听错,不用代码,“拖”出header于footer,其实很简单,选中tablView,在控件中找到View拖到tableView上,往最上方拖动,知道看到左右有两个圈的时候松手,这个View就是tableHeaderView了,同理,往最下方拖,就是tableFooterView。
1490498-cd74b79914fce993.gif
LaunchScreen.storyboard
从iOS8开始iPhone多了4.7"和5.5"的两种设备,这使得适配更加复杂,特别是设置启动图,如果考虑到横竖屏的话,要做好多张图,最重要的是,启动图是最占体积的东西,为了更好、更方面的配置启动图,LaunchScreen.storyboard出现了,简单来说,启动的时候会加载这个SB文件,我们可以同过它更方便的设置启动图,可以用auto layout减少启动图数量的使用,但此功能只支持iOS8及其以上的系统。
那么问题来了,我要想适配更低的系统怎么办?答:不用。
如何禁止该功能?
Storyboard Entry Point
如果我们用xcode6或者更高版本的xcode创建工程的话,你会发现自动就有了一个Main.storyboard
application:didFinishLaunchingWithOptions:中没有一行代码运行就没有问题,并不像之前那样,要创建window,指定rootViewController,这些是如何实现的?
1490498-b860cb27ad9a3659.jpgxcode自动配置了一个SB文件,而以上的这一切都有xcode自动帮我们完成了。
那么问题来了:一个SB是可以对应多个VC的,他选哪个VC作为window的rootViewController?答案是Storyboard Entry Point,这个东西就是用来指定那个作为rootViewController的,也就是说,xcode会找到表示为Storyboard Entry Point的那个VC加载它成为rootViewController,而以后的跳转就由我们之前介绍的方式:
1490498-f108e0c05c1f70b0.jpg
勾选就是设置了Storyboard Entry Point,设置了Storyboard Entry Point的VC会有一个向右的箭头指向它,注意你在Main Interface里选的SB文件中一定要有VC勾选了这个,不然xcode是不知道如何设置rootViewController的,你不用担心多选的问题,你如果选择一个新的VC,旧的那个VC就自然没有了Storyboard Entry Point,但是如果你又取消了勾选那么旧的VC并不会自动又添加Storyboard Entry Point的,要小心。
高冷用法
1490498-6baf58be612c8e95.jpgIBAction与IBOutlet
这是我们最常接触的两个,大家对它们已经有了很好的认识,这里只简单的说一下。
对于一个类来说,方法和属性(在这里属性与字段合在一起表示一个概念)是最重要的两个要素,而IBAction与IBOutlet就是分别标识方法与属性的,它们标识着由它们修饰的方法和属性是来自xib的,我猜它们是给编译器看的。
IBInspectable
在OC中使用IBInspectable,在swift中使用@IBInspectable
它是xcode6引入的新功能,它修饰的属性或者实例变量,会显示在xib中的属性栏中(Show the Attributes inspector),我们之前讲的东西都是xib是如何影响代码的,而IBInspectable是可以用代码影响xib的,可能我的表述不是很正确,还是看一个具体例子吧。
@interface ViewController : UIViewController
//gj_testFlag用IBInspectable修饰后,就能在xib中看到这个属性了,当然也可以用xib进行赋值了
@property (assign, nonatomic) IBInspectable BOOL gj_testFlag;
@end
```![1490498-0a46dcda9e2d9982.jpg](http:https://img.haomeiwen.com/i2318672/eca4b84372b52bf8.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
###IB_DESIGNABLE
在OC中将IB_DESIGNABLE写在@implementation前,在swift中将@IBDesignable写在class前
它也是xcode6引入的新功能,它的作用是可以在不运行的情况下把你的代码显示在xib或SB文件中。
两点说明:
1.这是一个针对UI显示的功能,所以只能是在UIView及其子类或者NSView及其子类上生效。
2.要想使IBDesignable起作用必须把代码写在drawRect里才能显示,同样的代码,我写在了awakeFromNib里就不会再xib中看出效果,只有写在了drawRect才可以。
举个例子:
我们建一个工程,新建一个TestView类继承自UIVIew,在Main.storyboard里拖一个View,class设置为TestView,背景设置成灰色。
![1490498-e7e06b420aa167ad.jpg](http:https://img.haomeiwen.com/i2318672/8003435c42d37fe8.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
IB_DESIGNABLE
@implementation TestView
- (void)drawRect:(CGRect)rect {
UIBezierPath *firtPath =
[UIBezierPath bezierPathWithOvalInRect:CGRectMake(10, 10, 180, 180)];
CAShapeLayer *shapeL = [CAShapeLayer layer];
shapeL.lineWidth = 20;
shapeL.path =firtPath.CGPath;
shapeL.strokeStart = 0;
shapeL.strokeEnd = 1;
shapeL.strokeColor = [UIColor yellowColor].CGColor;
shapeL.fillColor = [UIColor clearColor].CGColor;
[self.layer addSublayer:shapeL];
self.layer.cornerRadius = 30;
self.layer.masksToBounds = YES;
}
@end
![1490498-bb8d92cace1eaaab.jpg](http:https://img.haomeiwen.com/i2318672/a03e52aaefce99ac.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
###IBOutletCollection(ClassName):
将基于IBOutlet创建的对象放在一个NSarray里。
@property (strong, nonatomic) IBOutletCollection(UIView) NSArray *testViewArr;
创建了一个array,里面放的是用IBOutlet创建的UIView.
注意最好用strong进行修饰,而且如果你声明的不是NSArray,即便是UIColor,系统也不会报错,你打印这个color发现,系统用的还是NSArray。这个array的顺序是连线时候的顺序,但是不排除不同版本的xcode会改变这个顺序,所以最好不要依赖这个顺序。
也可以像拖IBOutlet那样创建:
###Files Owner的应用举例
有这样一个场景,VC中有一个textfield要设置inputAccessoryView属性,该属性的view显示起来很复杂,有多个按钮,每个按钮对应不同的事件。
一般的做法是用代码写一个这样的view赋值给inputAccessoryView属性,其实这个例子可以用xib实现的更优雅,不用写代码就可以完成(当然点击每个按钮后的事件处理代码是要自己写的)。
例子中要考虑的重点是:
如果创建了一个AccessoryView.xib去拖出这样一个view,虽然不用“画”UI了,但是我们要建一个AccessoryView.h、AccessoryView.m类去与xib文件对应,在AccessoryView.m中把它上面的按钮事件记录下来,一旦触发事件,要通过delegate或通知等其他形式把事件从AccessoryView类传递给VC类,这样使事情更加的麻烦了,如何解决?
有人会想:创建AccessoryView.h、AccessoryView.m是没有必要的,因为他们除了传递事件,根本没做任何事情,这样的话就不创建他们,只有AccessoryView.xib文件,然后把xib中的按钮分别拖动到VC类中建立起IBAction的“连线”关系,事情就搞定了。
这个思路很好,但是我们会发现,并不能实现AccessoryView.xib与VC中的“连线”,因为VC类根本不认识这个xib,因此该VC是不允许这个xib通过“连线”向它内部添加代码的,如何解决这个问题?——Files Owner!
将AccessoryView.xib的Files Owner指定成该VC的类,此时再拖“连线”到VC就可以了,这样xib中按钮的事件就能直接回调到VC中我们设置的方法里了。
![1490498-39be8c63b59f1de8.jpg](http:https://img.haomeiwen.com/i2318672/7f59af2081958963.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
xib文件是可以不依托于UIView子类、UIViewController子类单独使用的,只是这种情况比较少见,这是一个例子。