iOS开发中有一个常见的需求就是settings界面。一般来说这个界面是在UITableView的基础上做的。然而,直接使用UITableViewDelegate和UITableViewDataSource进行settings界面的开发显得非常麻烦。好在这是一个常见的需求,有不少libraries封装了这个功能。今天的源码选择了一个比较新的Swift库——Former。
1. 功能
Former最主要的就是对UITableView的Data Source和Delegate进行一系列封装,使得用户不需要直接实现一系列的protocol函数。相反,Former制定了一系列高阶的Protocol,用户需要遵守Former的Protocol便可以定制自己的Cell样式了。
除此之外,Former还封装了对键盘时间监听,当键盘弹出时自动对UITableView的inset和offset做出调整,保证正在编辑的cell不会被键盘遮住。另外,在滑动table view时,确保键盘隐藏。
import Former
final class ViewController: FormViewController {
override func viewDidLoad() {
super.viewDidLoad()
let labelRow = LabelRowFormer<FormLabelCell>()
.configure { row in
row.text = "Label Cell"
}.onSelected { row in
// Do Something
}
let inlinePickerRow = InlinePickerRowFormer<FormInlinePickerCell, Int>() {
$0.titleLabel.text = "Inline Picker Cell"
}.configure { row in
row.pickerItems = (1...5).map {
InlinePickerItem(title: "Option\($0)", value: Int($0))
}
}.onValueChanged { item in
// Do Something
}
let header = LabelViewFormer<FormLabelHeaderView>() { view in
view.titleLabel.text = "Label Header"
}
let section = SectionFormer(rowFormer: labelRow, inlinePickerRow)
.set(headerViewFormer: header)
former.append(sectionFormer: section)
}
}
Former的使用代码示例如上所示,只需要声明每个Row的Former类型,对应的Cell类,以及对former的事件相应,即可方便地完成settings界面的编写。
2. 结构

上图是简化版的Former的UML图。Former主要有以下几部分组成:
-
Former: 对
UTableViewDelegate
和UITableViewDataSource
的封装。具体每一个Section的行为和每一个Row的行为,由SectionFormer
和RowFormer
取得。通过add
,insert
,remove
系列方法对SectionFormer
进行管理。 -
SectionFormer: 对section的封装,每一个
Former
可以有多个SectionFormer
。SectionFormer
包含了section header和section footer的一些信息。每个SectionFormer
包含了一个RowFormer
数组,用以取得row的信息。通过SectionFormer
的add
,insert
,remove
系列方法可以对section下的RowFormer
进行管理。 -
RowFormer:
RowFormer
保存了具体每个cell的信息。它有若干个子类,不同种类的Form都有对应的一个子类。比如TextFieldRowFormer
是指带有UITextField
的former,SwitchRowFormer
是指带有UISwitch
的former。 -
FormableRow系列protocols: 每一种
RowFormer
的子类都带有对应的FormableRow
protocol。 比如TextFieldFormableRow
。这一系列的protocols主要是为了将UITableViewCell
和RowFormer
之间解耦。这组protocols规定了实现它们的UITableViewCell
必须要有哪些控件,比如TextFieldFormableRow
指定了实现了它的UITableViewCell
必须提供一个UITextField
和一个UILabel
。所有遵守这些protocols的UITableViewCell都可以集成到Former中。 -
FormCell及其子类: Former有一组默认的FormCell,它们实现了对应的
FormableRow
接口,一般来说使用这些默认的cell足以完成大部分界面了。由于逻辑部分已经都由RowFormer
承担了,这里的FormCell
纯粹是起显示的作用。
这样的结构设计,一方面满足了View和控制逻辑的完全解耦,另一方面也具备了扩展性,因为只要实现了FormableRow
系列接口,就可以将自己的Cell集成到Former中。
3. 实现细节
Cell的管理
首先,settings界面的cells和别的列表界面的cells有一个区别,就是我们可以不考虑它的复用。因为settings的项一般都是有限的。如果你一个settings界面有几百个cells,那就说明需求设计有问题。Former的设计便是省略了cell的复用,使得cell的状态变得简单许多。
从RowFormer
及其子类的实现中我们可以看到,我们不需要将一个UITableViewCell的实例交给它管理,而是只要告诉它Cell的类信息,比如类名,或者xib的名字。RowFormer会自动实例化出一个cell来。
其中,作者使用了泛型来获取cell的类信息。这方面作者做得非常巧妙。我们先来看一下RowFormer是如何初始化的。这里以TextFieldRowFormer
为例:
// In TextFieldRowFormer.swift
public required init(instantiateType: Former.InstantiateType = .Class, cellSetup: (T -> Void)? = nil) {
super.init(instantiateType: instantiateType, cellSetup: cellSetup)
}
// In ExampleViewController.swift
let textFieldRowFormer = TextFieldRowFormer<FormTextFieldCell>() {
$0.titleLabel.text = "Field\(index)"
$0.titleLabel.textColor = .formerColor()
$0.titleLabel.font = .boldSystemFontOfSize(16)
$0.textField.textColor = .formerSubColor()
$0.textField.font = .boldSystemFontOfSize(14)
$0.textField.inputAccessoryView = inputAccessoryView
$0.textField.returnKeyType = .Next
$0.tintColor = .formerColor()
}
其中,TextFieldRowFormer
本身是一个泛型类,在使用时,必须指定泛型类型。init
函数中,第二个参数是一个closure,它用来对初始化完毕的cell实例进行修改。此时,cellSetup
这个closure的第一个参数是一个cell实例,其实也就是泛型类型T
的对象,由RowFormer传入。如果不使用泛型的话,cellSetup
这个closure将不能知道即将传入的cell实例到底是什么类型,只知道它是UITableViewCell
,或者是TextFieldFormableRow
,那么用户只能在closure体里面进行强制转换了。
那么如何从泛型类型中获取类型信息进行cell的初始化呢?我们可以从一下TextFieldRowFormer
的父类BaseRowFormer
看起。
public class BaseRowFormer<T: UITableViewCell>: RowFormer {
// MARK: Public
public var cell: T {
return cellInstance as! T
}
required public init(
instantiateType: Former.InstantiateType = .Class,
cellSetup: (T -> Void)? = nil) {
super.init(
cellType: T.self,
instantiateType: instantiateType,
cellSetup: cellSetup
)
}
...
}
public class RowFormer {
...
public final let cellType: UITableViewCell.Type
...
internal init<T: UITableViewCell>(
cellType: T.Type,
instantiateType: Former.InstantiateType,
cellSetup: (T -> Void)? = nil) {
self.cellType = cellType
self.instantiateType = instantiateType
self.cellSetup = { cellSetup?(($0 as! T)) }
initialized()
}
...
// MARK: Internal
internal final var cellSetup: (UITableViewCell -> Void)?
...
}
我们可以看到,最顶层的RowFormer
的init
函数需要一个cellType
作为参数。而BaseRowFormer
通过T.self
得到泛型的具体类型,传到父类的init
函数中,这样就完成了对泛型类型的提取。
我们还可以看到,RowFormer
本身并不是一个泛型类。这是因为它是被SectionFormer
和Former
管理的,而这两个类已经不需要知道RowFormer里的cell的具体信息了,只需要知道它的cell是一个UITableViewCell
就可以了。这样一来,就可以简化上层代码了。
但是RowFormer.init
本身却是一个泛型函数。因为我们需要确保T
是UITableViewCell
的一种。
这段代码里还有一个非常奇怪的地方:
self.cellSetup = { cellSetup?(($0 as! T)) }
为什么作者要在这里包装一层呢?这是因为RowFormer
并不是泛型类,泛型部分到init
就结束了。而cellSetup
是需要被保存成instance variable的,但却已经拿不到T的信息了。所以self.cellSetup
被设置成了(UITableViewCell -> Void)?
类型。此时,init
函数中的cellSetup
参数类型是(T -> Void)?
,比self.cellSetup
的参数类型要精确,如果直接写self.cellSetup = cellSetup
会因为类型不匹配而编译不过,所以只能用这种很奇怪的写法了。
注意,RowFormer
系列的构造函数并不直接实例化一个cell对象,而只是取得cell的有关信息而已。真正的实例化是按需的。
我们已经知道,Former
这个类其实是实现了UITableViewDelegate
和UITableViewDataSource
的。在UITableView
回调Former
的cellForRowAtIndexPath:
方法时,Former
通过IndexPath
找到对应的RowFormer
,并且取得它的cellInstance
这个instance variable。实际上这个instance variable只是一个getter方法(computed property),它会在第一次调用它时产生并保存一个instance。
// In RowFormer.swift
internal final var cellInstance: UITableViewCell {
if _cellInstance == nil {
var cell: UITableViewCell?
switch instantiateType {
case .Class:
cell = cellType.init(style: .Default, reuseIdentifier: nil)
case .Nib(nibName: let nibName):
cell = NSBundle.mainBundle().loadNibNamed(nibName, owner: nil, options: nil).first as? UITableViewCell
assert(cell != nil, "[Former] Failed to load cell from nib (\(nibName)).")
case .NibBundle(nibName: let nibName, bundle: let bundle):
cell = bundle.loadNibNamed(nibName, owner: nil, options: nil).first as? UITableViewCell
assert(cell != nil, "[Former] Failed to load cell from nib (nibName: \(nibName), bundle: \(bundle)).")
}
_cellInstance = cell
cellInstanceInitialized(cell!)
cellSetup?(cell!)
}
return _cellInstance!
}
在上面的代码中,我们看到了在之前代码中出现但一直未提及的instantiateType
。这个type有三种类型:
public enum InstantiateType {
case Class
case Nib(nibName: String)
case NibBundle(nibName: String, bundle: NSBundle)
}
可以看到,Former支持通过Class,Nib或者Bundle+Nib的方式对Cell进行初始化。之前的RowFormer
系列的构造函数中,instantiateType
一直被默认设置成了.Class
。也就是通过类名来进行初始化。最关键的一步就是cell = cellType.init(style: .Default, reuseIdentifier: nil)
。在之前的代码里,我们看到cellType
是UITableViewCell.Type
类型的。所以我们可以断定,cellType存在init(style:, reuseIdentifier:)
这个方法,故使用此方法即可初始化一个实例。在初始化实例之后,会调用cellSetup
进行进一步的自定义。之后这个cell就被保存在_cellInstance
中,不再重复初始化了。这样做其实是实现了懒加载:在Former不询问这个RowFormer的cellInstance
前,不做cellInstance的初始化,询问后只做一次初始化。看起来跟lazy关键词很像。但由于在cell初始化时,需要使用其他的instance variables,所以无法使用lazy关键词。作者使用了这种这种的方法。
InlineForm
Former中有一类特殊的RowFormer,他们实现了叫做InlineForm
的protocol。这类form有个特点,就是点击之后能够展开。最典型的就是DatePicker
了。它的原理很简单,所有的InlineForm
都包含一个inlineRowFormer
。其实我觉得这个名字起的不好,应该叫expandedRowFormer
才对,因为它对应的是点击后展开的部分。Former
类中的didSelectRowAtIndexPath
对当前被点击的RowFormer
进行判断,如果是属于InlineForm
的,则会提取它的inlineRowFormer
,然后插入到table中。
在实现InlineForm
的子protocol ConfigurableInlineForm
是,作者使用了一个使用typealias的技巧。
Swift中,protocol是不支持使用泛型的,但是使用typealias,可以模仿出泛型的效果。
public protocol ConfigurableInlineForm: class, InlineForm {
// Needs to implements
typealias InlineCellType: UITableViewCell
// Needs NOT to implements
func inlineCellSetup(handler: (InlineCellType -> Void)) -> Self
func inlineCellUpdate(@noescape update: (InlineCellType -> Void)) -> Self
}
可以看到,里面使用了typealias InlineCellType: UITableViewCell
,但没有指定它等于哪个,下面方法的声明则又使用了这个typealias。如果要实现这个protocol,在实现时必须指定这个typealias的类型,比如:
public final class InlineDatePickerRowFormer<T: UITableViewCell where T: InlineDatePickerFormableRow>
: BaseRowFormer<T>, Formable, ConfigurableInlineForm {
public typealias InlineCellType = FormDatePickerCell
...
}
这样就达到了泛型的效果。
SelectorDatePickerFormableRow和SelectorPickerFormableRow
这两种Row(cell)也是比较特殊的,如果一个cell实现了这两个protocol中的一个,那么点击之后,底部会弹出一个date picker或者picker。

这个实现的原理,其实是将cell的inputView设置成了下面的那个picker view。picker view被当做键盘弹出来了。由于Former本来就实现了对键盘弹出消失事件的监听和处理,以保证table view不被遮住,所以这里并不需要做任何其他处理。
Fluent API
Former几乎所有的API设计都是链式API,也就是每个方法最后都返回的是self。这样一来可以一直用.
来调用下去。
其他语法细节
Former的作者对Swift的函数式API掌握得比较好,用了很多这样的API来简化代码。所谓的函数式API,很多是对list(Array)的操作,以及对tuple的运用。比如:
private final func setup() {
view.backgroundColor = .groupTableViewBackgroundColor()
view.insertSubview(tableView, atIndex: 0)
let tableConstraints = [
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-0-[table]-0-|",
options: [],
metrics: nil,
views: ["table": tableView]
),
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-0-[table]-0-|",
options: [],
metrics: nil,
views: ["table": tableView]
)
].flatMap { $0 }
view.addConstraints(tableConstraints)
}
使用flatMap
可以将[[a,b,c],[d,e,f]]
转化成[a,b,c,d,e,f]
。从而只用一次view.addConstraints(tableConstraints)
。
再比如,对某个control进行多个addTarget:action:forControlEvents
可以写成:
let events: [(Selector, UIControlEvents)] =
[("textChanged:", .EditingChanged),
("editingDidBegin:", .EditingDidBegin),
("editingDidEnd:", .EditingDidEnd)]
events.forEach {
textField.addTarget(self, action: $0.0, forControlEvents: $0.1)
}
再比如一个很常见的需求,就是如果某个变量不是nil,那么用它的值替换model里的值,如果是nil,则不对现有model对应数据做任何修改(常见于用户设置里,如果用户在密码栏中填了新密码,则修改密码,如果没有填,则维持老密码不变)。
最普通的做法是:
if password != nil { model.password = password}
而作者用了另外一种写法:
_ = password.map { model.password = $0 }
这样可行的原因是,Optional
的map
方法,如果Optional
是.None
,则直接返回nil,如果不是,则执行f(self)
。当password
是nil时,后面的block不会被执行。
另外,作者还使用了一些语法级的currying来简化代码。很可惜Swift的语法级currying将在3.0版本中被删除,所以暂时不需要去管。
总结
Former是一个比较中规中矩的控件封装,目的是让用户做settings界面更加方便。它并没有做成最近热门的reactive的形式,但已经非常方便了。总的来说,作者对泛型和函数式API使用得非常到位,代码也简洁易懂,算是非常优秀的作品了。唯一不足的是,作者是日本人,大概英文不好,里面有些注释有很明显的语法错误。不过并不影响代码阅读和学习。
网友评论
这两种Row(cell)也是比较特殊的,如果一个cell实现了这两个protocol中的一个,那么点击之后,底部会弹出一个date picker或者picker
我想知道这个怎么让picker从底部弹出???我没找到相关的属性或者协议,求解答
<<< PickerInlineRow("duration"){(row : PickerInlineRow<String>) -> Void in
row.title = "竞拍时长"
row.options = []
for i in 1...10{
row.options.append("\(i)")
}
// row.options.first
}
这个玩意默认是在中间展开的,我怎么拿到那个cell如你所说去更改inputview亦或是有什么属性方法去更改弹出的方式??
这两种Row(cell)也是比较特殊的,如果一个cell实现了这两个protocol中的一个,那么点击之后,底部会弹出一个date picker或者picker
我想知道这个怎么让picker从底部弹出???我没找到相关的属性或者协议,求解答