不一样于国外,StoryBoard
从面世到现在饱受国内开发者的质疑,质疑的理由不少,什么不利于多人协做啊,隐藏了UI细节啊,出问题不容易测试,下降执行效率啊等等。此文就是针对这些问题的举例和剖析。git
StoryBoard
和 Xib
有什么区别?StoryBoard
和 Xib
都是用来分离UI样式代码,改善视图代码重用率,增长所见即所得,下降视图测试繁复度的视图系列化工具,swift
- 其中
Xib
以视图View
为主,StoryBoard
以控制器Controller
及其之间的关系,以及和视图View
的关系为主。
实际使用例子参见《纯Swift项目-Xib | StoryBoard 设备适配技巧》或其余StoryBoard
文章数组
StoryBoard
和 Xib
不利于多人协做,git
合并代码容易冲突,且难以处理?这个是诋毁StoryBoard
最多的理由,也是看上去
最充分的理由。最显著的就是下图这种失败的例子。缓存
在一个Storyboard
中,大量的Controller
控制器和Segue
连线彰显着错综复杂的UI关系,令人望而生畏或者难以维护。app
Storyboard
的锅,仅仅是使用者对工具的滥用!没错,就是滥用
,不管是Storyboard
也好,纯代码也罢,它们的本质都是工具,工具自己没有正义或邪恶,影响工具的是使用者。哪怕是用纯代码开发,若是没有命名规范,肆意的嵌套if
,不遵照MVC或者MVVM等开发模式,不区分开发环境与生产环境,这样写出来的代码又何谈可维护性,和多人协做呢?ide
Storyboard
才不算滥用?避免滥用,最好的方法就是定制规范,就好像代码中的诸多规范同样。每一个团队可能有本身不一样的喜爱,我在此抛砖引玉,列出咱们团队使用Storyboard
的规范,供你们参考。工具
每一个模块独立Storyboard | 每一个Storyboard只应该有一个主VC和同页的子VC,主VC不该存在2个以上 |
---|---|
![]() |
![]() |
- 一个项目中,Storyboard不应是孤立存在的,应该像
MVP
模式那样,每一个页面都有独立的Storyboard,每一个Storyboard只应该有一个主VC和同页的子VC,主VC不该存在2个以上。(绝大多数状况下,一个Storyboard上只应该有一个VC)- 页面间的
Segue
连线应该使用Stroyboard Reference Scene
,UITabBarController
的子页由于复杂度应该当成主VC处置- 视图的初始样式应尽可能在Storyboard上属性面板中设置,非极特殊状况,布局也应在Storyboard上使用各类约束配合完成。这样有利于视图样式和视图代码分离,有利于视图代码重用性和兼容性提升。
- 对于逻辑复杂的VC,应添加Object对象,并绑定相应的类来分离逻辑代码。
- 对于圆角,背景色,阴影等
CALayer
的样式,应该使用扩展或子类化实例的形式,使用@IBInspectable
属性关键字,在Storyboard属性面板中设定初始样式。- 对于自定义视图,应使用
@IBDesignable
关键字保障在在Storyboard上所见即所得!
使用以上原则,只要任务分工合理,基本上不存在多人同时修改同一个Storyboard
的状况,就算配合失误偶然发生,精简的Storyboard其代码量也不大,借助文件比较工具很容易就能处理git冲突。布局
说到底,臃肿的
Storyboard
和臃肿的ViewController
同样,都是难以维护且容易git冲突的。惟一的解决方案就是有节制的使用工具。post
StoryBoard
和 Xib
隐藏了UI细节,且容易致使ViewController
臃肿?与其说StoryBoard
和 Xib
隐藏了UI细节,倒不如说苹果是但愿经过他们来引导开发者正确的使用 视图 和 控制器 ,他们建立视图实例的时候都是经过性能
required init?(coder aDecoder: NSCoder) {
}
复制代码
构造方法建立视图实例。全部初始样式都是在属性面板中设置的值,经过
func setValue(_ value: Any?, forUndefinedKey key: String) {
......
}
复制代码
来赋值给视图对应的属性。
至于说致使
ViewController
臃肿,更是荒谬,StoryBoard
提供了多种方案来分离代码,只不过不少人不知道而已。
拿美团的主页UI举例
这样的首页较为复杂,正常布局的话须要多个CollectionView
和一个UITableView
若是这些视图的Delegate
都由ViewController
来实现,天然显得臃肿且混乱。
通常手写派会分出3个ChildViewController
来解决臃肿问题,难道Storyboard
就作不到么?
答案是否认的,很早的版本,苹果就给出了上图中的解决方案。一个占位的容器视图指向子控制器的Embed Segue
按住Control
键连线到想要包含的子控制器,占位视图的实例==子控制器的view
(子控制器根视图)
选择Embed
连线方式后,子控制器 的尺寸变化成跟占位视图同样的尺寸
这样咱们能够将功能图标的CollectionView
的代码放到这第一个子控制器上,CollectionViewDelegate
、CollectionViewDataSource
等代码也由子控制器实现
同理,优惠专区能够再添加一个Container View
,指向第二个子控制器。
Container View
建立的ChildViewController
如何与主ViewController
传参或互相调用?
ChildViewController
能够经过 self.parent(Swift)|| self.parentViewController(OC)来拿到主ViewController
的实例。 主ViewController
能够经过 self.chilren(Swift) || self.childViewControllers(OC)来拿到ChildViewController
的实例,它是一个数组,顺序等同于占位视图再视图层次中的顺序。
值得一提的是,经过此种方式建立的ChildViewController
,其构造方法晚于主ViewController
,但生命周期中的viewDidLoad
则早于主ViewController
, 所以在ChildViewController
中的viewDidLoad
方法中,self.parent 是nil
,这时不能拿到主ViewController
实例。若是须要在初始化的时候拿到主ViewController
的实例,则应该在主ViewController``viewDidLoad
方法中,调用ChildViewController
的特定方法,把 self 当参数传过去。
将它添加到控制器之上。
它的本质是一个继承自NSObject的子类,咱们彻底能够把它当成一个小功能模块的控制器。
class FeaturesController: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
@IBOutlet weak var collectionView:UICollectionView!
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
<#code#>
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
<#code#>
}
}
复制代码
在Storyboard
上选中这个Object
,绑定上面的类
Object
,在弹出的菜单中连线
右键CollectionView
设置 Delegate 和 DataSource 等的连线
在主ViewController
中如需调用这个模块的方法或者传参
class HomeController: UIViewController {
@IBOutlet weak var featuresController:FeaturesController!
override func viewDidLoad() {
super.viewDidLoad()
featuresController.datas = [....]
featuresController.collectionView.reloadData()
}
}
复制代码
完成连线,同理,若是一个页面须要多个子模块,能够在Storyboard
上拖入多个Object
,并绑定不一样的模块控制类,相对于占位的Container View
和ChildViewController
方法,Object
方法在传参或互相调用方面,更加简便。缺点是没有ChildViewController
的生命周期方法,如需使用viewWillAppear
等,须要在主ViewController
的viewWillAppear
中,调用Object
的自定义方法。
经过上面的2种方法不难看出,并不是是Storyboard
形成ViewController
代码臃肿,而是由于设计不当致使,就算你不用Storyboard
,把全部功能都写在一个ViewController
里同样臃肿。这都是使用者决定的,并不是Storyboard
的责任!
StoryBoard
和 Xib
出了问题不容易测试?这个问题其实问的很模糊,我也是咨询了不少人才知道,他们所谓的问题不容易测试,是指以下两种状况:
- 修改或删除 @IBOutlet 的变量名时,对应的
Storyboard
上未作处理,致使运行时崩溃,崩溃内容看不懂!- 绑定的类名改变时,对应的
Storyboard
上未作处理,致使运行时崩溃,崩溃内容看不懂!
其实只要知道,苹果是如何把Storyboard
的xml
解析成视图,崩溃的错误内容也就容易看懂了 以前提到过,视图构造使用的是下面这个方法
required init?(coder aDecoder: NSCoder) {
}
复制代码
若是绑定的类名改变输出错误:
- Unknown class _TtC11ProjectName14HomeController in Interface Builder file. // Swift
- Unknown class HomeController in Interface Builder file. // Objective C
经过上面的错误提示Interface Builder file
就是指经过Storyboard
或者Xib
构建视图或者控制器,但找不到名为HomeController
的控制器,看到这里就应该明白,咱们某个Storyboard
上绑定了名为HomeController
的控制器,但代码中找不到,多是更名或者删除了。这时能够全局搜素一下
在搜出来的结果中能够看到,是在Main.storyboard
上绑定了HomeController
,Test.swift
文件中定义了该类,可是由于更名因此没法找到。
这样的问题不用Storyboard
就能够避免么?答案是否认的,由于重构代码的时候,改了一处忽略它处的例子比比皆是。哪怕纯代码也是同样,所以,若是须要修改类名或者变量名,应该善用Xcode
的重构功能,而不是简单的直接修改。
这样修改类名或者变量名是,Storyboard
或者Xib
上绑定或连线的内容也会同步改变。就不会出错了。
同理,@IBOutlet 连线的属性经过下面的方法给视图赋值
func setValue(_ value: Any?, forUndefinedKey key: String) {
......
}
复制代码
若是变量名改变的时候,会出现以下错误:
- *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<HomeController 0x7fbd0ce20c40> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key featuresController.'
这个方法找不到对应的属性时,就会抛出异常, 这里就是指找不到featuresController
属性,经过全局搜索能够发现,代码中改了名字,
解决的方法一样是删掉对应的连线或者修改变量名时使用重构
因而可知,所谓的不容易测试,彻底是由于重构不谨慎且对构造过程不理解,不然仍是很容易定位问题且修改的。并且重构代码时利用Xcode重构功能
的话,连问题都不会出现
StoryBoard
和 Xib
下降执行效率?这个问题看起来好像是那么回事,StoryBoard
和 Xib
本质上是XML
,要解析成视图就须要反序列化,必然没有直接代码建立速度高,但这只是感受上,实际上有多少影响呢?咱们来测试一下:
var controllers:[ViewController] = []
let count = 30000
controllers.reserveCapacity(count)
guard let sb = storyboard else { return }
var beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = sb.instantiateViewController(withIdentifier: "ViewController") as! ViewController
controllers.append(vc)
}
print("Storyboard建立\(count)次用时", CACurrentMediaTime() - beginTime)
controllers.removeAll(keepingCapacity: true)
beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = ViewController()
controllers.append(vc)
}
print("纯代码建立\(count)次用时", CACurrentMediaTime() - beginTime)
复制代码
第一次使用了3万次,结果输出
- Storyboard建立30000次用时 8.648092089919373
- 纯代码建立30000次用时 27.226440161000937
咱们看到了什么?从Storyboard
建立居然比纯代码更快?简直不敢相信本身的眼睛,并且差距这么大必定是有什么神奇的事情发生,为了验证个人想法,我又将Storyboard
建立复制了一次
var controllers:[ViewController] = []
let count = 30000
controllers.reserveCapacity(count)
guard let sb = storyboard else { return }
var beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = sb.instantiateViewController(withIdentifier: "ViewController") as! ViewController
controllers.append(vc)
}
print("Storyboard建立\(count)次用时", CACurrentMediaTime() - beginTime)
controllers = []
controllers.reserveCapacity(count)
beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = ViewController()
controllers.append(vc)
}
print("纯代码建立\(count)次用时", CACurrentMediaTime() - beginTime)
controllers = []
controllers.reserveCapacity(count)
beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = sb.instantiateViewController(withIdentifier: "ViewController") as! ViewController
controllers.append(vc)
}
print("Storyboard建立\(count)次用时", CACurrentMediaTime() - beginTime)
复制代码
输出结果以下,并且屡次运行结果相近,多是由于随着内存使用率提升,电脑性能在下降,影响告终论,但无论怎么说,大量测试空的ViewController
在这种状况下确实比纯代码建立更快。
- Storyboard建立30000次用时 8.513293381780386
- 纯代码建立30000次用时 27.19225306995213
- Storyboard建立30000次用时 25.9916725079529
这个结果是如何出现的,不妨大胆猜想一下,多是因为苹果在对象屡次建立的状况下,Storyboard
可能存在缓存复刻机制,来提高效率,而纯代码并无这样的优化。为了验证猜想,咱们逐渐下降数量级。
- Storyboard建立3000次用时 0.20833597797900438
- 纯代码建立3000次用时 0.2654381438624114
- Storyboard建立3000次用时 0.34943647705949843
- Storyboard建立300次用时 0.010981905972585082
- 纯代码建立300次用时 0.005475352052599192
- Storyboard建立300次用时 0.014193600043654442
- Storyboard建立30次用时 0.0016030301339924335
- 纯代码建立30次用时 0.00031192018650472164
- Storyboard建立30次用时 0.001034758985042572
- Storyboard建立10次用时 0.0009886820334941149
- 纯代码建立10次用时 0.0001325791236013174
- Storyboard建立10次用时 0.0014422889798879623
上述结果果真验证了咱们的猜想,随着次数的减小,Storyboard
建立的速度逐渐低于存代码建立,但单次耗时仍然低于万分之一秒,这种效率是不会让用户有任何感知的,况且重复建立比纯代码还有优点,所以,这一条也不算StoryBoard
和 Xib
的缺点
StoryBoard
和 Xib
拖动和设置约束布局很难精确?不易修改?我想,这种言论多是由于不太熟悉Interface Builder
的功能和操做形成的,仅仅实验了几回不得其门而入就放弃了。
实际上约束布局是一个很强大的功能,能够解决绝大多数(98%)布局适配问题,98%
这个数并非随便给出的,不少人以为达不到这个比例是由于对约束理解较少,仍是按照之前的autolayoutMask的方式使用约束,所以不少布局问题还在用代码计算,可实际上约束功能十分强大,目前没法经过约束直接解决,必须代码辅助的问题微乎其微。
但与之相对的是约束的概念较多,依赖人脑思考很容易产生遗漏,这样在运行的时候就会各类报错或显示异常,所以用纯代码写约束,反复运行调试视图样式尺寸十分常见,并且有些页面较深,测试起来十分麻烦。
而使用StoryBoard
或 Xib
就不一样了,缺乏约束或者约束冲突直接就有错误提示,适配不一样设备能够直接在Interface Builder
上切换测试,效率不知高了多少倍,准确性也高了不少
若是须要详细了解在
StoryBoard
或Xib
上使用约束的技巧,能够参考文章《纯Swift项目-Xib | StoryBoard 设备适配技巧》及 《纯Swift项目-Xib | StoryBoard 约束使用技巧》或其余相关文章。
StoryBoard
和 Xib
虽然不是毫完好点,但优点远大于付出,值得学习研究!