Preface(废话)
在iOS
开发中一直有一个很具争议的话题,那就是界面布局到底是代码好还是使用IB(xib/storyboard)
好?
有些人觉得手撸代码,才叫coder
,使用代码的编译速度快(快不快苹果说了算)
有些人觉得IB
的开发效率不容置疑,而且能减少界面代码对工程的污染
还有一些人觉得计算frame
挺有意思…
好吧,如果还在使用initWithFrame()
计算界面的布局,请在本文的留言区默默的扣个1...
本文不打算讨论哪个好,毕竟这么些年了,没有得出啥好的结论,代码的还是啪啪;IB
也玩的很开心;
咱们把所有的方式都罗列一下,各位看官各取所需
手撸代码
Frame党
在autolayout
没有出来的时候,你使用frame
也就不说啥了,因为在那个时候手机屏幕比较单一,就只有320*480一个,frame
随便计算,但是现在这个时代那么多屏幕,你还是这个...
如果只是简单界面,你frame
一下也没啥,请看下面的示例代码:
// 分割线
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(8, 58, Width - 16, 1)];
label1.backgroundColor = SEPARATORLINE;
[self addSubview:label1];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(8, 116, Width - 16, 1)];
label2.backgroundColor = SEPARATORLINE;
[self addSubview:label2];
// 任务目标
UILabel *aim = [[UILabel alloc] initWithFrame:CGRectMake(8, 8, 70, 21)];
[self setLabel:aim WithTitle:@"任务目标:" withFont:FONT(15) withColor:PD_NAVI_COLOR];
_taskAimLabel = [[UILabel alloc] initWithFrame:CGRectMake(8, 29, Width - 16, 21)];
_taskAimLabel.font = FONT(13);
[self addSubview:_taskAimLabel];
// 任务奖励
UILabel *award = [[UILabel alloc] initWithFrame:CGRectMake(8, 67, Width - 16, 21)];
[self setLabel:award WithTitle:@"任务奖励:" withFont:FONT(15) withColor:PD_NAVI_COLOR];
_taskAwardLabel = [[UILabel alloc] initWithFrame:CGRectMake(8, 88, Width - 16, 21)];
_taskAwardLabel.font = FONT(13);
[self addSubview:_taskAwardLabel];
// 任务描述
UILabel *des = [[UILabel alloc] initWithFrame:CGRectMake(8, 124, Width - 16, 21)];
[self setLabel:des WithTitle:@"任务描述:" withFont:FONT(15) withColor:PD_NAVI_COLOR];
_taskDescriptionLabel = [[UILabel alloc] initWithFrame:CGRectMake(8, 145, Width - 16, 42)];
_taskDescriptionLabel.font = FONT(13);
_taskDescriptionLabel.numberOfLines = 2;
[self addSubview:_taskDescriptionLabel];
... ...
我只想问,同学你真的不累么?你不累我这个维护者看着都累,如果是一个很长且布局不重复的界面,你咋办?
估计自己算着算着就晕了
所以,对于frame
党,我只想说,你需要一个计算器...
手写Constraints
其实手写Constraints
也是相当麻烦的,主要是因为Constraints
有着非人的语法,给个例子大家随意感受一下
constraint = [
NSLayoutConstraint
constraintWithItem:testButton
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:00.0f
];
[self.view addConstraint:constraint];
constraint = [
NSLayoutConstraint
constraintWithItem:testButton
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:-20.0f
];
[self.view addConstraint:constraint];
上面这段代码,只是添加了两个约束,试想一下,如果我们要写一个比较复杂的界面时,得写多长的代码?
不过,上面的代码可以优化
NSDictionary *viewsDic = NSDictionaryOfVariableBindings(deleteButton,cancelButton,nextButton);
NSArray *constraints = nil;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:
@"H:|-25-[deleteButton(==cancelButton@700)]-(>=8)-[cancelButton(140)]-[nextButton(nextButtonWidth)]-rectY-|"
options:NSLayoutFormatAlignAllTop
metrics:@{@"rectY":@5,@"nextButtonWidth":@30}
views:viewsDic];
[self.view addConstraints:constraints];
上面的代码看懂了么?
我自己都看不懂...
但是总有一些程序员和别的程序员不一样,他们能有不一样的能力,他们叫做大神,大神给我们封装了Masonry/Snapkit
Masonry/Snapkit
Masonry是一个轻量级的布局框架,拥有自己的描述语法,采用更优雅的链式语法封装自动布局,简洁明了,并具有高可读性,而且同时支持iOS
和 Max OS X
;Snapkit是Masonry
的swift
版本,语法也差不多
先看一个例子再说:
// masonry
[testButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self);
make.size.mas_equalTo(CGSizeMake(50, 50));
make.bottom.equalTo(hanupLabel.mas_top).offset(-10);
}];
// snapkit
testButton.snp_makeConstraints(closure: { make in
make.centerX.equalTo(self);
make.size.mas_equalTo(CGSizeMake(50, 50));
make.bottom.equalTo(hanupLabel.mas_top).offset(-10);
})
控件直接调用mas_makeConstraints
方法,在block
中使用MASConstraintMaker
进行相应的布局就行了,非常简洁
最重要的是可读性很高,在masonry
里面有几点需要注意的地方:
- 必须先把控件添加到父视图以后才能进行约束,不然会
crash
-
mas_equalTo
,在约束值为具体的数值(CGSize
,CGPoint
等也是)的时候需要使用这个 - 内存管理,约束的
block
里面有时候会引起隐式内存泄露 - 重复约束,或约束出现冲突的时候,在控制台会有
log
输出,发现以后改掉就好了 - 如果多个约束在一行时,使用连接符语法(
and
),增强代码的可读性
上面几条是笔者在使用过程中总结的一些经验,大家可以参考一下,其实在这里内存管理方面,很值得说到;不过笔者在之前已经写过相关OC内存管理和Swift内存管理的文章了大家可以出门左转进去看看,就能找到解决隐式内存泄露问题的办法😄
Neon
Neon其实也是一个swift
版的autolayout
框架,但是为什么把他单独拿出来说一下呢?Neon
除了具有Snapkit
的大部分优点以外,还有一个强大的功能,放一张官方图,你们随意感受一下
来个栗子吧:
view1.anchorToEdge(.Top, padding: padding, width: size, height: size)
view2.anchorToEdge(.Left, padding: padding, width: size, height: size)
view3.anchorToEdge(.Bottom, padding: padding, width: size, height: size)
view4.anchorToEdge(.Right, padding: padding, width: size, height: size)
上面这段代码的效果图如下:
有没有被震撼到,amazing
😱有木有,而且Neon
的作者经常直播写Neon
库,感兴趣的可以去LivingCode看看,关于Neon
更多的使用方法,大家可以去github上仔细研究,个人认为是非常不错的
XXAutoLayout
其实在github
上还有很多autolayout
的框架,xxxAutoLayout
多了去了,但是我想说的是,适合自己的才是最好的,而且建议各位看官在选框架的时候,要多方考量,star
数量,issue
数量,支持度等等
不管如何,手写代码咱们先说到这里,下面看看Interface Builder
Interface Builder
关于IB
的争议真的是太多了,文章长度都能绕地球xx圈;当然了,那都是别人的看法,那都是别人的观点,那都是别人写的文章;下面咱们具体看看到底好不好用,然后做结论
在autolayout
没有出来之前,就已经有IB
了,但是那个时候IB
不能算强大,只在Autolayout
出来以后,IB
才真的崭露头角,下面的很多例子我通过gif
的方式给大家演示如何使用IB
基本用法
一个简单的居中
上面这个例子演示了让一个100*100的view
在屏幕中居中的方法,使用了垂直和水平居中,并且设置大小为100*100,约束的重点在于,给出的约束一定要能计算出控件的位置和大小,缺一不可;有时候给出的约束可能没有明确指出某个约束的值,但是可以通过给定条件计算出来,也是可以的,比如下面这个例子,做一个水平三等分
这里要记住一个快捷键command + option + ‘=‘(update constraint constant)
简单,快捷,而且没有代码,任何约束相关的代码都被隐藏在了IB
的文件里面,只有你需要使用这个view
的时候把他拖线出去就行了,如果需要修改控件的属性,直接在右侧的attributes inspector
里面修改就可以了.
有些inspector
里面没有给出的属性,也可以在identity inspector
里面通过user defined runtime attributes
修改
比如给一个view添加圆角,attributes inspector里面就没有给出相应的属性,如果使用代码修改你需要写一下两行代码:
view.layer.cornerRadius = 10.0
view.layer.masksToBounds = true
使用user defined runtime attributes
,你只需要这样就可以了:
user defined runtime attributes
里面可添加的属性有很多,大家可以大胆去探索
在xib/storyboard
里面不仅仅可以做autolayout
和属性设置,还可以控件拖到代码里面使用,设置控件的代理,拖出控件自带的方法(在xib/storyboard
里面叫action
),以及给控件添加手势等等,看下面的demo
:
这里有一点要说一下,我们做好的约束(constraint
)也是可以拖出去的,而且可修改,很多使用xib/storyboard
很久的同学都不知道,因为笔者之前见过这样的开发者:
他界面布局使用xib/storyboard
,然后需要动态修改的时候他又使用Masonry
修改(其实是又重新约束了一遍😢)
所以这里提醒大家,需要修改某个约束的时候直接拖出去,修改那条约束的constant
值就OK
啦
WOW✨✨✨✨
解放了有木有🎉🎉🎉
再也不用看那些臃肿的界面布局代码了
笔者第一次使用这些东西的时候,想起了那个夕阳下的奔跑,那是我逝去的青春(内牛满面😂)...
Scrollview
在xib
上做scrollview
约束的时候比较头疼,因为一不小心就会导致出来的界面不能滚动.这是因为,没有添加contentview
的原因,如果没有contentview
,scrollview
就无法计算出自己的contentsize
,所以无法滚动;
笔者在xib
上使用scrollview
的原则是,放完scrollview
的时候就放contentview
,然后在考虑子控件如何摆,各位同学仔细看下面的gif
这里具体的原理和解析细说起来都能另开一篇博客了,所以这里不做详细说明,有兴趣的同学可以搜一下,挺简单的
通过上面的demo
,我们可以学到不只是scrollview
在xib
中的约束方法,还有如下几点:
- 通过修改视图本身的长度为
freedom
,来约束长view
- 快速添加约束,在
demo
的结尾笔者添加了2个button
,并木有使用约束界面来添加,通过快捷键command + shift + option + ‘=‘
来快速添加的
SizeClass
sizeclass
是对不同尺寸的屏幕的区分,sizeclass
把不同尺寸(包括横屏和竖屏)的屏幕进行了分类,无论是iPhone
还是iPad
设备,其宽度和高度都被划分为三种类型:Compact
(紧凑)、Regular
(正常)、Any
(任意)我们只要针对于某一类型的屏幕进行布局,那么布局出来的界面可以显示在属于该类型的所有尺寸的屏幕上。
在开发中我们经常会遇到横屏处理问题,比如我们在横屏的时候会改变布局方式,比如隐藏和添加控件,下面这个例子演示一下在xib
上使用sizeclass
在横屏情况下添加一个button
,并且在横屏情况下修改控件的属性,比如颜色:
在Xcode8
以前,xib/storyboard
上的sizeclass
设置界面有些不同,但是使用方法还是一样的;
就上面这样简单的case
,如果你用代码写,目测至少50+代码,这一点IB
还是有优势的
Storyboard快速搭建静态页面
所谓静态页面就是那些不需要加载数据,或就算加载数据页面也还是长的一样,不会发生变化,比如微信的发现界面
大家随便一看就知道,tableview
嘛
嗯,没错,但是如何写这个界面才好呢 ?这个tableview
有4个section
,有5个cell
,好了分析完毕,开始撸代码吧,我们截取其中一部分代码看看(cellForRow
里面的)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath)
switch indexPath.section {
case 0:
cell.imageView?.image = UIImage(named: "")
cell.textLabel?.text = ""
case 1:
cell.imageView?.image = UIImage(named: "")
cell.textLabel?.text = ""
case 2:
cell.imageView?.image = UIImage(named: "")
cell.textLabel?.text = ""
case 3:
switch indexPath.row {
case 0:
cell.imageView?.image = UIImage(named: "")
cell.textLabel?.text = ""
case 1:
cell.imageView?.image = UIImage(named: "")
cell.textLabel?.text = ""
}
}
}
就仅仅一个cellForRow
就已经23行了,更别说didSelect
方法里面的逻辑了,不用多说,再看看使用storyboard
来做这个页面的过程,包括cell
的点击事件都在storyboard
里面完成,这里有一点需要注意,如果你的静态页面是使用tableview
列表形式,那么控制器必须是tableviewcontroller
,请看下面的gif-demo
:
简直是不可思议的快捷,如果这些我们使用代码写的话,我可以想想,单单switch
代码你得写多少行?
什么?你用if - else
?
呃......
或许你也需要一个计算器之类的?
Storyboard的多人开发问题
在Xcode 7
之前,storyboard
在团队开发的时候非常不友好,因为一个storyboard
文件有很多个界面,多人开发的话,就需要多个人都打开这个storyboard
文件,而在Xcode 7
之前你只要点开storyboard
文件,虽然你啥都没有做,但是storyboard
文件里面出现一个Modify
标记,然后你提交代码的时候会conflict
,很多团队因为这个问题放弃storyboard
,转而使用xib
,甚至连xib
都不用了,直接手撸.
多可惜, storyboard
的很多便捷特性都用不了了...
只从Xcode 7
以后,苹果针对这个问题推出了storyboard reference
来解决storyboard
的多人开发问题
storyboard reference
的功能就是拆分storyboard
,把storyboard
拆分成一个个小模块,这样每个开发人员对应一个模块,各自分离,互不相干
下面通过拆分tabbarcontroller
的几个分支演示一下storyboard reference
使用方法:
通过上面的例子我们可以看到,storyboard reference
把tabbar
拆成了2个module
并分别放在两个storyboard
里面,这样负责相应module
开发的同学可以在自己的storyboard
里面放心的开发,不用在担心提交时候的conflict
代码加载自定义的xib/Storyboard
在实际开发中,我们的视图跳转逻辑会很复杂,有些视图不能够通过链接segue
直接跳转到,或者有时候segue
已经链接了很多了,在链接下去,会显得很杂乱,这个时候你可以考虑使用代码加载xib/storyboard
来完成跳转逻辑
非常简单,只有短短的几行代码
// 加载xib
// 这里返回的是一个数组,数组包含了改xib中所有的view,顺序跟你创建时的先后顺序一致
let xibView = Bundle.main.loadNibNamed("CustomXib", owner: self, options: nil)?.first
self.view.addSubview(xibView as! UIView)
// 加载storyboard
// 获取storyboard
let storyboard = UIStoryboard(name: "CustomStoryboard", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(withIdentifier: "CustomStoryboard")
在使用代码加载storyboard
中的控制器的时候,要注意在对应的storyboard
上要设置identity
,方式如下图:
Interface Builder的一些问题
1.编译速度慢,这个问题是不可否认的,确实比纯代码的稍微慢一些,不过我觉得如果你使用的固态硬盘(SSD
)的话,这个问题应该还好,笔者15寸MacBook Pro
,平时编译速度还行,所以这方面的感受不太深
2.打包可能会增大包体积,这个问题在今天看来还能是个问题么 ?现在的App
随便弄弄上MB
了,增加个2-3MB
的体积,根本没有啥感觉,最关键的是,现在WIFI
的普及率太高了,4G
流量也增加了,所以担心包体积的同学,可能还生活在5年前的2G3G
时代
3.关于维护,有很多人说在维护xib/storyboard
开发的项目的时候有些困难,找不到view
关系,看不到代码等,其实我想说,那是因为你真的没有用心的去学习xib/storyboard
,如果作为维护者的你也对xib/storyboard
非常熟悉,你会发现维护xib/storyboard
开发的项目会比纯代码的相对轻松的,至少你不需要在1000+的代码中去找某个button
添加的action
;同样的xib/storyboard
中,你只需要右单击对应的button
就能看到action
的名称,直接定位到目标了
Conclusion
看完本文以后,大部分读者应该有一个猜测:作者一定是一个IB
深度用户
好吧,我承认,从我学会使用IB
之后的所有项目都使用了该技术,因为我觉得真的很好用,因为IB
可以减少我们在界面上花费的时间,如此我们可以更好的专注于业务和结构设计等工作
同时也没有放弃代码,我也在使用Msonry,Snapkit
和Neon,偶尔也会frame
一下😝
其实IB有很多强大的功能本文并未提及和深入,比如@IBInspectable
,Segue
,Autolayout
动画等
很多强大功能,大家可以自己去挖掘,网上的资源足矣
各种布局方法基本上都涉及了,大家根据自己的情况选择,还是那句话:适合自己的才是最好的
当然笔者还是推荐xib+storyboard
😝
关于xib+storyboard
或代码Autolayout
欢迎大家一起讨论,如有错误及时指出及时修正
生命不息,折腾不止...
I'm not a real coder, but i love it so much!