源码导读——Former

作者: __Hokuang__ | 来源:发表于2016-01-29 21:41 被阅读243次

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的UML图。Former主要有以下几部分组成:

  1. Former: 对UTableViewDelegateUITableViewDataSource的封装。具体每一个Section的行为和每一个Row的行为,由SectionFormerRowFormer取得。通过add, insert, remove系列方法对SectionFormer进行管理。
  2. SectionFormer: 对section的封装,每一个Former可以有多个SectionFormerSectionFormer包含了section header和section footer的一些信息。每个SectionFormer包含了一个RowFormer数组,用以取得row的信息。通过SectionFormeradd, insert, remove系列方法可以对section下的RowFormer进行管理。
  3. RowFormer: RowFormer保存了具体每个cell的信息。它有若干个子类,不同种类的Form都有对应的一个子类。比如TextFieldRowFormer是指带有UITextField的former,SwitchRowFormer是指带有UISwitch的former。
  4. FormableRow系列protocols: 每一种RowFormer的子类都带有对应的FormableRow protocol。 比如TextFieldFormableRow。这一系列的protocols主要是为了将UITableViewCellRowFormer之间解耦。这组protocols规定了实现它们的UITableViewCell必须要有哪些控件,比如TextFieldFormableRow指定了实现了它的UITableViewCell必须提供一个UITextField和一个UILabel。所有遵守这些protocols的UITableViewCell都可以集成到Former中。
  5. 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)?
    ...
}

我们可以看到,最顶层的RowFormerinit函数需要一个cellType作为参数。而BaseRowFormer通过T.self得到泛型的具体类型,传到父类的init函数中,这样就完成了对泛型类型的提取。

我们还可以看到,RowFormer本身并不是一个泛型类。这是因为它是被SectionFormerFormer管理的,而这两个类已经不需要知道RowFormer里的cell的具体信息了,只需要知道它的cell是一个UITableViewCell就可以了。这样一来,就可以简化上层代码了。

但是RowFormer.init本身却是一个泛型函数。因为我们需要确保TUITableViewCell的一种。

这段代码里还有一个非常奇怪的地方:
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这个类其实是实现了UITableViewDelegateUITableViewDataSource的。在UITableView回调FormercellForRowAtIndexPath:方法时,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)。在之前的代码里,我们看到cellTypeUITableViewCell.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。

SelectorPickerFormableRow

这个实现的原理,其实是将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 }
这样可行的原因是,Optionalmap方法,如果Optional.None,则直接返回nil,如果不是,则执行f(self)。当password是nil时,后面的block不会被执行。

另外,作者还使用了一些语法级的currying来简化代码。很可惜Swift的语法级currying将在3.0版本中被删除,所以暂时不需要去管。

总结

Former是一个比较中规中矩的控件封装,目的是让用户做settings界面更加方便。它并没有做成最近热门的reactive的形式,但已经非常方便了。总的来说,作者对泛型和函数式API使用得非常到位,代码也简洁易懂,算是非常优秀的作品了。唯一不足的是,作者是日本人,大概英文不好,里面有些注释有很明显的语法错误。不过并不影响代码阅读和学习。

相关文章

  • 源码导读——Former

    iOS开发中有一个常见的需求就是settings界面。一般来说这个界面是在UITableView的基础上做的。然而...

  • Webpack Loader源码导读之css-loader

    原文地址:Webpack Loader源码导读之css-loader 在上一篇Webpack Loader源码导读...

  • Spring 源码导读

    做为Java开源世界的第一框架,Spring已经成为事实上的Java EE开发标准Spring框架最根本的使命是简...

  • Spring 源码导读

    java高级架构师 做为Java开源世界的第一框架,Spring已经成为事实上的Java EE开发标准Spring...

  • YYModel 源码导读

    YYModel 是一个把 Json 数据转换成 model 的一个轻量级工具。本文将深入源码来谈谈YYModel是...

  • MJExtension源码导读

    由于时间问题,暂时先放出来整理好的思维导图

  • Collection源码导读

    List ArrayList ArrayList 是 List 接口的典型实现类、主要实现类;本质上, Array...

  • AQS源码导读

    前言 AQS全称:AbstractQueuedSynchronizer,抽象的队列同步器,和synchronize...

  • former presidents

    正文 Selling Trump: a profitable post-presidency like no ot...

  • Tomcat源码导读 - Connector,Container

    源码导读 Connector Container StandardEngine、StandardContext、S...

网友评论

  • 不辣先生:SelectorDatePickerFormableRow和SelectorPickerFormableRow
    这两种Row(cell)也是比较特殊的,如果一个cell实现了这两个protocol中的一个,那么点击之后,底部会弹出一个date picker或者picker
    我想知道这个怎么让picker从底部弹出???我没找到相关的属性或者协议,求解答
    __Hokuang__:@上了年纪的少年 因为你用错了cell和former, PickerInlineRow就是在中间展开的,你需要SelectorDetePickerFormableRow
    不辣先生:@__Hokuang__ 哦,但是怎么拿到那个cell去修改了
    <<< 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亦或是有什么属性方法去更改弹出的方式??
    __Hokuang__:@上了年纪的少年 它的原理是 inputView,https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/InputViews/InputViews.html ,原理就是将一个cell的inputView设置成一个picker view,然后将其变成firstResponder.
  • 不辣先生:大神,麻烦解答一下上面的问题可以么?? :pray:
  • 不辣先生:InlineForm让他pickerView从底部弹出来能详细说说么
    不辣先生:@__Hokuang__ SelectorDatePickerFormableRow和SelectorPickerFormableRow
    这两种Row(cell)也是比较特殊的,如果一个cell实现了这两个protocol中的一个,那么点击之后,底部会弹出一个date picker或者picker
    我想知道这个怎么让picker从底部弹出???我没找到相关的属性或者协议,求解答
    __Hokuang__:@上了年纪的少年 你想知道什么?

本文标题:源码导读——Former

本文链接:https://www.haomeiwen.com/subject/szujkttx.html