在更新 Best Before 的过程中,我决定重写 ForwardStrategy 的部分。过去 ForwardStrategy 中不同的转发目标所储存的信息其实是一样的,只是不同的转发目标会隐藏其不包含的属性。因此其对应的表单也是这样设计的,表单中其实包含了所有属性对应的 cell,但是在选中某些目标时,其中一些 cell 被隐藏起来了。

举个例子吧:比如说我们有 Things 和 Mail 两种转发目标,当我们选中 Things 时,因为它并没有收件人,所以其对应的 cell 被隐藏起来了。当我们切换到 Mail,收件人就得以重见天日,但截止日期却被隐藏起来了。这个方案当然是 work 的,但是却有一个明显的缺点,一旦我们需要添加新的转发目标,或更新旧有的转发目标时出现了新的属性时,我们就需要修改模型,并在表单中增加新的 cell 了。

因此在这次更新中,我决定将转发目标的属性以 JSON 为格式储存,这也避免了不同转发目标混用属性的尴尬。表单方面也需要作出修改。窒息观察可以发现,这些属性有几个固定的类型:

1
2
3
4
5
6
7
enum ForwardFormElement {
case highlightTextView(title: String, placeholder: String?, bind: PropertyRefTo<String>)
case textView(title: String, placeholder: String?, bind: PropertyRefTo<String>)
case textField(title: String, placeholder: String?, keyboardType: UIKeyboardType, bind: PropertyRefTo<String>)
case picker(title: String, options: [String], bind: PropertyRefTo<Int>)
case toggle(title: String, bind: PropertyRefTo<Bool>)
}

我们只需要提供每一种转发目标的 [ForwardFormElement],就能依次利用 Form Builder 生成相应的表单了。同时也为了契合我在用的 Form Builder 的特点,我希望在生成每一个 Row 时能够通过它的 onChange 方法将变动绑定到转发目标的相关属性上。这才有了本篇文章的主题。

先梳理一下,我希望的结果是:

  1. 转发目标提供 [ForwardFormElement] 给 Form Builder
  2. ForwardFormElement 中包含了一个对转发目标属性的弱引用
  3. Form Builder 生成 Row 时通过 onChange 方法绑定属性
    row.onChange { bind.val = $0 }
  4. bind.val 改变时同时改变转发目标的相关属性(很可能是值类型)

那么接下来就是怎么实现 bind: PropertyRefTo<T> 的问题了。

很简单,Swift 4 给我们带来了 KeyPath subscription,只要我们持有转发目标的弱引用,然后再存对应的 KeyPath 就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PropertyRefTo<V> {
var val: V?
}

final class PropertyRef<O: AnyObject, V>: PropertyRefTo<V> {
weak var object: O?
let keyPath: ReferenceWritableKeyPath<O, V>

init(_ object: O, _ keyPath: ReferenceWritableKeyPath<O, V>) {
self.object = object
self.keyPath = keyPath
}

override var val: V? {
get {
return object?[keyPath: keyPath]
}
set {
guard let v = newValue else { return }
object?[keyPath: keyPath] = v
}
}
}

能用吗?能用。但是我还有一个额外的需求,有一些对应 case picker(title: String, options: [String], bind: PropertyRefTo<Int>) 的属性是 Enum,但 Swift 并没有 Generic Case 这样的东西,所以我们要让 PropertyRef 能够按照我们的要求将其 val 转换成我们需要的类型。显而易见,不提供 objectkeyPath,而是提供 val 的 getter 和 setter 不就好了吗。

1
2
3
4
5
6
7
8
9
10
init(_ object: O, getter: @escaping (O)->V?, setter: @escaping (O, V)->Void) {
self.getter = {
[weak object] in guard let object = object else { return nil }
return getter(object)
}
self.setter = {
[weak object] newValue in guard let object = object else { return }
setter(object, newValue)
}
}

我们在 block 里通过 capture list 弱引用 object,因为这么写了,所以我们要求 O: AnyObject。最后 Key Path 这种这么方便的东西,我们也可以提供一个 convenience initializer。

1
2
3
convenience init(_ object: O, _ keyPath: ReferenceWritableKeyPath<O, V>) {
self.init(object, getter: { $0[keyPath: keyPath] }, setter: { $0[keyPath: keyPath] = $1 } )
}

至此我们就能获得一个 AnyObject 的非引用类型的属性的引用啦。

随后代码就可以写成

1
2
3
4
5
6
7
8
9
.highlightTextView(
title: Localized.This.Form.Things.notes,
placeholder: Localized.This.Form.Things.notesPlaceholder,
bind: PropertyRef(self, \.notes)),
.picker(
title: Localized.This.Form.Things.deadline,
options: DeadlineType.allTitles,
bind: PropertyRef(self, getter: { $0.deadlineType.identifier }
setter: { $0.deadlineType = DeadlineType(identifier: $1) })),