一、前言
-
抱歉这段时间比较忙,博客好久没更新了,今天有空就谈谈一种界面搭建方式吧!本文所说得复杂界面针对的是表单界面,表单界面常见的就是
UITableView
和UICollectionView
,因此本框架也是针对这两个进行封装,完整框架是使用Swift 3.0
编写,当然也有Objective-C
版本,不过功能目前相对少点,待完善。 -
至于说复杂,是针对以后的界面维护(比如界面元素的增删改)、数据源的更新(内部刷新以及外部刷新),如果是一般的做法,这种操作都会大大增加控制器的代码量,逻辑紊乱,控制器就十分臃肿,迭代几个版本之后,改起来估计就要哭了。
-
如下图,数据以及界面元素更新都涉及到,但我控制器的代码只有200多行(
Objective-C
编写),操作模块变得十分轻松,gitHub地址 觉得OK就点个Star呗。
二、界面元素变更
-
这里的界面元素变更,就是说表单界面需要动态插入、删除、修改某一个模块,或者交换两个模块的位置等等。
-
常见的做法是,因为数据源是在控制器中获取,那么操作数据源一般也会在控制器中处理,所以控制器处理界面元素变更,就需要写很多逻辑代码!如果此时有类似的控制器也需要处理,那么就造成代码冗余了,想想都可怕。那么此时就会想,如果界面是一个一个独立模块搭建而成,本文称为Component,每个Component自己处理自己的逻辑,意味着一个界面维护一个Component集合。至于外部需要修改界面元素,就只需要操作这个集合就好,对应数组的
addObject
、remove
、replace
、exchange
即可,详细后面会说。
三、数据源更新
此时说得数据源更新是针对一个Component来区分的,因为上文提过,界面的搭建是一个一个独立Component来构造。
-
1、内部更新:什么时候需要内部更新?比如当你一个Component里面带有状态更新(好比如按钮的点击选中)、输入内容更新等等,这时候就只需要Component自己处理就好,控制器也不需要关心这个处理过程,只需要关心最终处理后的结果。比如上图中的所有输入、按钮的点击,控制器是不会做任何处理的,独立的Component只需要最终告诉控制器,目前的文本是什么,按钮的状态是什么。
-
2、外部更新:外部更新就是由外界更新模块,常见的就是服务端拉去数据,控制器控制去更新指定的模块,或者模块与模块之间的通讯,也是通过控制器去实现更新,比如上图中的
选择标签
、视频标签
以及自定义标签
,三个Component模块的通讯,数据更新的操作发起是通过控制器,当然,具体处理逻辑还是Component自己实现,因为控制器根本不关心逻辑的处理细节,只在乎结果(控制器就是这么吊),日后的维护只需要修改对应的模块即可。这点就类似胖Model
的做法了。
此时值得提一下,因为数据更新就必须获取到指定的Component,当然你会想到遍历Component集合,判断当前是哪个Component类,但是有一个问题,如果整个表单都是用同一个Component,只是数据展示不一样而已。那么你遍历拿不到具体的哪个Component,因此内部是引入了一个 Component Identifier
的东西,这个identifier并不是类似cell的重用标识,而是表示Component的唯一标识
四、如何实现Component化
- 上图就一目了然了
-
1、Component只负责管理
Cell
、CellHeaderView
、CellFooter
,就是对应TableView
的一个组,那么一个表单的构成,就是由多个 Component 组成 -
2、此时引入了一个
Handler
去处理Component
,但前面说了,一个表单维护一个Component集合,表单的创建一般都在Controller
里面,那就意味着一个Component集合就通过Controller
去管理维护,其实是不好的! - (1)这种处理方法,本质上还是由控制器去处理一系列操作逻辑,那么
Component
的作用就几乎为零。 - (2)当然,通过继承控制器来解决代码冗余也没毛病,但我觉得,这并没有从根本上瘦身控制器。我个人也不太喜欢继承,当然,并不是不使用继承!因此引入
Handler
,一个Handler
专门处理一个表单的所有Component
的增删改操作,然后控制器去维护一个Handler
。
五、面向协议编程
因为iOS中常见表单有两种:一种使用TableView
,一种使用CollectionView
。两个表单都有共通的地方,例如:cell、headerView、footerView 的注册事件、dataSource数据源事件、delegate事件等等。那么共通的事件不可能每个类都写一遍的吧!那么此时就有两种方法了:
-
继承基类:继承这种方式在这里其实没什么毛病,因为某些公共的方法都是必须的,基类可以方便统一管理
-
协议:部分方法虽然都差不多,但可能形参不一样,返回不一样,那就把这类方法使用一个协议去管理,一方面可以方便移除,如果是基类继承就没办法移除了,当然
swift
有泛型处理,此时也可以统一管理不一样的入参以及返回,只需要确保遵守同一个协议就行。但这样外部使用就需要做类型判断,使用起来相对麻烦了
因此本文是一分开两个协议,协议中就是一些数据源事件以及代理事件;然后公共的就使用继承基类的方式实现,比如注册事件(cell、header、footer的注册都在这处理)、刷新Component事件等。所以本框架的目录结构如下:
六、框架分析
1、Component
-
(1)、上文说了,一个Component就是对应一个表单的Section,因此必须对应有Section表示当前Component所在下标,包含Cell headerView、Cell、Cell footerView三块,因此要处理cell,必须要传入当前的表单(TableView或CollectionView),当然还有很重要的
Component Identifier
,如果外界不指定,框架默认会有对应的identifier,如果是相同的Component,那么Identifier就会根据Component的section
拼接后缀。 -
(2)、注意,虽然传入的表单对象,但表单的数据源以及代理并不是Component去做的,Component只是做了第一层数据处理,处理注册事件、展示控件的创建(Cell、Cell headerView、Cell footerView)以及基本数据配置(Cell、Cell headerView、Cell footer 的高度)等
-
(3)、使用的时候,不建议直接使用基类(BaseComponent)创建,可以理解为它是一个抽象类,先初始化一个子类的Component,然后重写一些父类的方法,比如注册的
register
,数据源的numberOfItems
、cellForItem
等等
override func register() {
super.register()
self.collectionView?.registerNib(DemoCollectionViewCell.self, withReuseIdentifier: cellIdentifier)
}
override func numberOfItems() -> NSInteger {
return 12
}
override func cellForItem(at item: Int) -> UICollectionViewCell {
let cell : DemoCollectionViewCell = super.cellForItem(at: item) as! DemoCollectionViewCell
cell.backgroundColor = UIColor.red
cell.textLabel.text = stringFromSize(at: item)
return cell
}
2、Handler
上文说了,Handler的作用就是做Component和Controller的中介,Controller维护一个Handler,而Handler就负责帮Controller处理一系列逻辑操作。
-
(1)、Handler通过维护一个Component集合来管理各个Component的逻辑操作,当然,Component集合的来源还是需要Controller提供,然后通过Component集合来实现表单的数据源方法、真正处理相关控件的展示、以及表单的展示样式等。
-
(2)、上文中提过一个重要的点,就是
Component Identifier
,此时Handler内部维护一个表,通过外界或者内部提供的Component Identifier
来绑定对应的Component,后续就很容易通过唯一的Identifier来获取对应的Component,进而处理多个Component的通讯问题。 -
(3)、此时为了简便Component的操作(这个就是对应界面的元素变更),框架对应两个协议,这里为什么不用泛型而是写两套协议,原因跟上文提及的一样,为了让外界调用少些逻辑判断。协议中提供了Component的各种操作,应该都能满足大众需求,这部分代码没有做注释,看Api完全看得懂哈
- (4)、用法很简单,在对应需要创建表单的界面中强引用一个
Handler
,设置数据源给Handler
,如果你需要处理Cell、Cell headerView、Cell footerView的点击事件,遵守代码并设置Handler
的代理为当前的Controller,然后实现相关方法即可,当然,前提下你还是需要创建表单对象的
private(set) var handler : FLTableViewHandler = FLTableViewHandler()
var components : Array<FLTableBaseComponent> = [] {
didSet {
handler.components = components
}
}
handler.delegate = self
3、Controller
当然,虽然不喜欢使用继承,但很多时候继承确实好用,因此也提供了一个BaseController,此时如果你的界面只是一个表单,你完全可以继承自它,然后通过它的Handler
去处理表单界面的元素变更以及Component之间的通讯
七、细节功能点
-
1、本文提供了针对CollectionView表单的一种自定义
Flowlayout
,目前只有两种情况,左对齐布局或者是系统的中心对齐布局,并实现了CollectionView表单的Cell的HeaderView和FooterView的计算显示,简化Api的调用 -
2、针对TableView表单,Cell headerView、Cell footerView的代理方法中只能返回
String
类型,除非自定义HeaderView、FooterView,框架中新增了可以直接返回NSMutableAttributedString
,方便使用富文本显示 -
3、框架中做了很多数据判断,避免出现数据操作造成的崩溃
-
4、本文虽然主要针对是
Swift
,但也提供了针对Objective-C
版本,但目前Objective-C
只有针对TableView
表单的处理,当然,CollectionView表单处理是一样的思路。
八、总结
-
1、本文并没有过多讲解具体代码的实现,而是偏向于讲解思路,减少代码冗余,而且极其方便后续开发维护,想看具体演示,请看 Demo。
-
2、如果上文有说得不对的地方或者有问题建议,请大家留言,如果你觉得我文章不错,欢迎关注我,点个like和star,谢谢支持!