复杂的表单界面?

搭积木般轻松

Posted by gitKong on July 30, 2017

一、前言

  • 抱歉这段时间比较忙,博客好久没更新了,今天有空就谈谈一种界面搭建方式吧!本文所说得复杂界面针对的是表单界面,表单界面常见的就是UITableViewUICollectionView,因此本框架也是针对这两个进行封装,完整框架是使用Swift 3.0 编写,当然也有Objective-C 版本,不过功能目前相对少点,待完善。

  • 至于说复杂,是针对以后的界面维护(比如界面元素的增删改)、数据源的更新(内部刷新以及外部刷新),如果是一般的做法,这种操作都会大大增加控制器的代码量,逻辑紊乱,控制器就十分臃肿,迭代几个版本之后,改起来估计就要哭了。

  • 如下图,数据以及界面元素更新都涉及到,但我控制器的代码只有200多行(Objective-C 编写),操作模块变得十分轻松,gitHub地址 觉得OK就点个Star呗。

示例演示


二、界面元素变更

  • 这里的界面元素变更,就是说表单界面需要动态插入、删除、修改某一个模块,或者交换两个模块的位置等等。

  • 常见的做法是,因为数据源是在控制器中获取,那么操作数据源一般也会在控制器中处理,所以控制器处理界面元素变更,就需要写很多逻辑代码!如果此时有类似的控制器也需要处理,那么就造成代码冗余了,想想都可怕。那么此时就会想,如果界面是一个一个独立模块搭建而成,本文称为Component,每个Component自己处理自己的逻辑,意味着一个界面维护一个Component集合。至于外部需要修改界面元素,就只需要操作这个集合就好,对应数组的addObjectremovereplaceexchange 即可,详细后面会说。


三、数据源更新

此时说得数据源更新是针对一个Component来区分的,因为上文提过,界面的搭建是一个一个独立Component来构造。

  • 1、内部更新:什么时候需要内部更新?比如当你一个Component里面带有状态更新(好比如按钮的点击选中)、输入内容更新等等,这时候就只需要Component自己处理就好,控制器也不需要关心这个处理过程,只需要关心最终处理后的结果。比如上图中的所有输入、按钮的点击,控制器是不会做任何处理的,独立的Component只需要最终告诉控制器,目前的文本是什么,按钮的状态是什么。

  • 2、外部更新:外部更新就是由外界更新模块,常见的就是服务端拉去数据,控制器控制去更新指定的模块,或者模块与模块之间的通讯,也是通过控制器去实现更新,比如上图中的选择标签视频标签以及自定义标签,三个Component模块的通讯,数据更新的操作发起是通过控制器,当然,具体处理逻辑还是Component自己实现,因为控制器根本不关心逻辑的处理细节,只在乎结果(控制器就是这么吊),日后的维护只需要修改对应的模块即可。这点就类似胖Model的做法了。

此时值得提一下,因为数据更新就必须获取到指定的Component,当然你会想到遍历Component集合,判断当前是哪个Component类,但是有一个问题,如果整个表单都是用同一个Component,只是数据展示不一样而已。那么你遍历拿不到具体的哪个Component,因此内部是引入了一个 Component Identifier 的东西,这个identifier并不是类似cell的重用标识,而是表示Component的唯一标识


四、如何实现Component化

  • 上图就一目了然了

Process

  • 1、Component只负责管理 CellCellHeaderViewCellFooter,就是对应 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

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,数据源的numberOfItemscellForItem等等

Component协议部分Api

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完全看得懂哈

Componet集合的逻辑操作

  • (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,谢谢支持!

gitHub地址