纯Swift项目-Xib | StoryBoard 多人协做技巧

不一样于国外,StoryBoard从面世到现在饱受国内开发者的质疑,质疑的理由不少,什么不利于多人协做啊,隐藏了UI细节啊,出问题不容易测试,下降执行效率啊等等。此文就是针对这些问题的举例和剖析。git

StoryBoardXib 有什么区别?

StoryBoardXib 都是用来分离UI样式代码,改善视图代码重用率,增长所见即所得,下降视图测试繁复度的视图系列化工具,swift

  1. 其中Xib以视图View为主,
  2. StoryBoard 以控制器Controller及其之间的关系,以及和视图View的关系为主。

实际使用例子参见《纯Swift项目-Xib | StoryBoard 设备适配技巧》或其余StoryBoard文章数组

StoryBoardXib 不利于多人协做,git合并代码容易冲突,且难以处理?

这个是诋毁StoryBoard最多的理由,也是看上去最充分的理由。最显著的就是下图这种失败的例子。缓存

Storyboard不利图片

在一个Storyboard中,大量的Controller控制器和Segue连线彰显着错综复杂的UI关系,令人望而生畏或者难以维护。app

但这并不该该是Storyboard的锅,仅仅是使用者对工具的滥用!

没错,就是滥用,不管是Storyboard也好,纯代码也罢,它们的本质都是工具,工具自己没有正义或邪恶,影响工具的是使用者。哪怕是用纯代码开发,若是没有命名规范,肆意的嵌套if,不遵照MVC或者MVVM等开发模式,不区分开发环境与生产环境,这样写出来的代码又何谈可维护性,和多人协做呢?ide

那么反过来讲,如何使用Storyboard才不算滥用?

避免滥用,最好的方法就是定制规范,就好像代码中的诸多规范同样。每一个团队可能有本身不一样的喜爱,我在此抛砖引玉,列出咱们团队使用Storyboard的规范,供你们参考。工具

每一个模块独立Storyboard 每一个Storyboard只应该有一个主VC和同页的子VC,主VC不该存在2个以上
  1. 一个项目中,Storyboard不应是孤立存在的,应该像MVP模式那样,每一个页面都有独立的Storyboard,每一个Storyboard只应该有一个主VC和同页的子VC,主VC不该存在2个以上。(绝大多数状况下,一个Storyboard上只应该有一个VC
  2. 页面间的Segue连线应该使用Stroyboard Reference SceneUITabBarController的子页由于复杂度应该当成主VC处置
  3. 视图的初始样式应尽可能在Storyboard上属性面板中设置,非极特殊状况,布局也应在Storyboard上使用各类约束配合完成。这样有利于视图样式和视图代码分离,有利于视图代码重用性和兼容性提升。
  4. 对于逻辑复杂的VC,应添加Object对象,并绑定相应的类来分离逻辑代码。
  5. 对于圆角,背景色,阴影等CALayer的样式,应该使用扩展或子类化实例的形式,使用@IBInspectable属性关键字,在Storyboard属性面板中设定初始样式。
  6. 对于自定义视图,应使用@IBDesignable关键字保障在在Storyboard上所见即所得!

使用以上原则,只要任务分工合理,基本上不存在多人同时修改同一个Storyboard的状况,就算配合失误偶然发生,精简的Storyboard其代码量也不大,借助文件比较工具很容易就能处理git冲突。布局

说到底,臃肿的Storyboard和臃肿的ViewController同样,都是难以维护且容易git冲突的。惟一的解决方案就是有节制的使用工具。post

StoryBoardXib 隐藏了UI细节,且容易致使ViewController臃肿?

与其说StoryBoardXib 隐藏了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的代码放到这第一个子控制器上,CollectionViewDelegateCollectionViewDataSource等代码也由子控制器实现

同理,优惠专区能够再添加一个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.parentnil,这时不能拿到主ViewController实例。若是须要在初始化的时候拿到主ViewController的实例,则应该在主ViewController``viewDidLoad方法中,调用ChildViewController的特定方法,把 self 当参数传过去。


  • 除此以外还可使用Object对象

将它添加到控制器之上。

它的本质是一个继承自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 设置 DelegateDataSource 等的连线

在主ViewController中如需调用这个模块的方法或者传参

class HomeController: UIViewController {
    
    @IBOutlet weak var featuresController:FeaturesController!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        featuresController.datas = [....]
        featuresController.collectionView.reloadData()
    }
    
}
复制代码

完成连线,同理,若是一个页面须要多个子模块,能够在Storyboard上拖入多个Object,并绑定不一样的模块控制类,相对于占位的Container ViewChildViewController方法,Object方法在传参或互相调用方面,更加简便。缺点是没有ChildViewController的生命周期方法,如需使用viewWillAppear等,须要在主ViewControllerviewWillAppear中,调用Object的自定义方法。

经过上面的2种方法不难看出,并不是是Storyboard形成ViewController代码臃肿,而是由于设计不当致使,就算你不用Storyboard,把全部功能都写在一个ViewController里同样臃肿。这都是使用者决定的,并不是Storyboard的责任!

StoryBoardXib 出了问题不容易测试?

这个问题其实问的很模糊,我也是咨询了不少人才知道,他们所谓的问题不容易测试,是指以下两种状况:

  1. 修改或删除 @IBOutlet 的变量名时,对应的Storyboard上未作处理,致使运行时崩溃,崩溃内容看不懂!
  2. 绑定的类名改变时,对应的Storyboard上未作处理,致使运行时崩溃,崩溃内容看不懂!

其实只要知道,苹果是如何把Storyboardxml解析成视图,崩溃的错误内容也就容易看懂了 以前提到过,视图构造使用的是下面这个方法

required init?(coder aDecoder: NSCoder) {
    
    }
复制代码

若是绑定的类名改变输出错误:

  1. Unknown class _TtC11ProjectName14HomeController in Interface Builder file. // Swift
  2. Unknown class HomeController in Interface Builder file. // Objective C

经过上面的错误提示Interface Builder file就是指经过Storyboard或者Xib构建视图或者控制器,但找不到名为HomeController的控制器,看到这里就应该明白,咱们某个Storyboard上绑定了名为HomeController的控制器,但代码中找不到,多是更名或者删除了。这时能够全局搜素一下

在搜出来的结果中能够看到,是在Main.storyboard上绑定了HomeControllerTest.swift文件中定义了该类,可是由于更名因此没法找到。

这样的问题不用Storyboard就能够避免么?答案是否认的,由于重构代码的时候,改了一处忽略它处的例子比比皆是。哪怕纯代码也是同样,所以,若是须要修改类名或者变量名,应该善用Xcode的重构功能,而不是简单的直接修改。

这样修改类名或者变量名是,Storyboard或者Xib上绑定或连线的内容也会同步改变。就不会出错了。

同理,@IBOutlet 连线的属性经过下面的方法给视图赋值

func setValue(_ value: Any?, forUndefinedKey key: String) {
        ......
    }
复制代码

若是变量名改变的时候,会出现以下错误:

  1. *** 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重构功能的话,连问题都不会出现

StoryBoardXib 下降执行效率?

这个问题看起来好像是那么回事,StoryBoardXib本质上是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万次,结果输出

  1. Storyboard建立30000次用时 8.648092089919373
  2. 纯代码建立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在这种状况下确实比纯代码建立更快。

  1. Storyboard建立30000次用时 8.513293381780386
  2. 纯代码建立30000次用时 27.19225306995213
  3. Storyboard建立30000次用时 25.9916725079529

这个结果是如何出现的,不妨大胆猜想一下,多是因为苹果在对象屡次建立的状况下,Storyboard可能存在缓存复刻机制,来提高效率,而纯代码并无这样的优化。为了验证猜想,咱们逐渐下降数量级。

  1. Storyboard建立3000次用时 0.20833597797900438
  2. 纯代码建立3000次用时 0.2654381438624114
  3. Storyboard建立3000次用时 0.34943647705949843
  1. Storyboard建立300次用时 0.010981905972585082
  2. 纯代码建立300次用时 0.005475352052599192
  3. Storyboard建立300次用时 0.014193600043654442
  1. Storyboard建立30次用时 0.0016030301339924335
  2. 纯代码建立30次用时 0.00031192018650472164
  3. Storyboard建立30次用时 0.001034758985042572
  1. Storyboard建立10次用时 0.0009886820334941149
  2. 纯代码建立10次用时 0.0001325791236013174
  3. Storyboard建立10次用时 0.0014422889798879623

上述结果果真验证了咱们的猜想,随着次数的减小,Storyboard建立的速度逐渐低于存代码建立,但单次耗时仍然低于万分之一秒,这种效率是不会让用户有任何感知的,况且重复建立比纯代码还有优点,所以,这一条也不算StoryBoardXib的缺点

StoryBoardXib 拖动和设置约束布局很难精确?不易修改?

我想,这种言论多是由于不太熟悉Interface Builder的功能和操做形成的,仅仅实验了几回不得其门而入就放弃了。

实际上约束布局是一个很强大的功能,能够解决绝大多数(98%)布局适配问题,98%这个数并非随便给出的,不少人以为达不到这个比例是由于对约束理解较少,仍是按照之前的autolayoutMask的方式使用约束,所以不少布局问题还在用代码计算,可实际上约束功能十分强大,目前没法经过约束直接解决,必须代码辅助的问题微乎其微。

但与之相对的是约束的概念较多,依赖人脑思考很容易产生遗漏,这样在运行的时候就会各类报错或显示异常,所以用纯代码写约束,反复运行调试视图样式尺寸十分常见,并且有些页面较深,测试起来十分麻烦。

而使用StoryBoardXib就不一样了,缺乏约束或者约束冲突直接就有错误提示,适配不一样设备能够直接在Interface Builder上切换测试,效率不知高了多少倍,准确性也高了不少

若是须要详细了解在StoryBoardXib上使用约束的技巧,能够参考文章《纯Swift项目-Xib | StoryBoard 设备适配技巧》及 《纯Swift项目-Xib | StoryBoard 约束使用技巧》或其余相关文章。

总结,StoryBoardXib虽然不是毫完好点,但优点远大于付出,值得学习研究!

相关文章
相关标签/搜索