FormsKitDavid Rodrigues,@dmcrodrigues
Babylon Health
1
Forms? !
2
3
Butbefore starting some
context...
4
Managing state can bean
extremelyhardtask
» Mutability
» Observation
» Thread-safety
5
To buildaformweare
especiallyinterested in
mutabilityand observation
6
Thread-safetyisalso
fundamentalbut outofscope
forthistalk
7
final class Property<Value> {
private var handlers: [(Value) -> Void] = []
var value: Value {
didSet { handlers.forEach { $0(value) } }
}
init(value: Value) {
self.value = value
}
func observe(_ handler: @escaping (Value) -> Void) {
self.handlers.append(handler)
handler(value)
}
func map<T>(_ transform: @escaping (Value) -> T) -> Property<T> {
let property = Property<T>(value: transform(value))
observe { [weak property] value in property?.value = transform(value) }
return property
}
}
8
⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
Please notethis isafairly
simple implementation !
⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
9
Propertygive usanicewayto observe
changes
1> let a = Property(value: 1)
2> a.observe { value in print("Value: (value)") }
"Value: 1"
3> a.value = a.value + 2
"Value: 3"
10
Andto derive newstates
1> let a = Property(value: 1.0)
2> let b = a.map { value in "(value) as String" }
4> b.value
"1.0 as String"
5> a.value = a.value * 10
6> a.value
10.0
7> b.value
"10.0 as String"
11
Backto forms...
12
13
// Sign-Up Table View DataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 12
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
// Facebook button
case 1:
// Or text
case 2:
// First Name
case 3:
// Last Name
...
}
}
14
Adding or removing elements requires:
» update the total number of elements
» shift all indices affected
Moving elements requires:
» shift all indices affected
15
Stillmanageable?
16
17
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
// Account selected
case 1:
switch consultantType {
case .gp:
switch dateAndTimeAvailable {
case true:
...
case false:
...
}
case .specialist:
...
}
case ???:
...
...
}
}
18
Buthowcanwe getthose
fancyanimationswhen
something changes?
19
20
Index pathsare
aweak system,
hard to manage
and maintain21
Especially with
dynamic changes
22
Whatthen?
23
Let's forgeteverythingand
startfrom scratch...
24
What'saform?
25
Aform isacollection
of components following
asequential order
26
27
A form isa collection
ofcomponents following
a sequentialorder
28
Colection ofcomponents
withasequentialorder?
!
29
ArrayAn ordered, random-access collection
1> let array = [0, 1, 2]
2> array
[
0,
1,
2
]
30
We can representaform
withacollection of
components
31
» Text
» Text Input
» Password
» Selection
» Buttons
» ...
32
enum Component {
case text(String)
case textInput(placeholder: String, ...)
case password(placeholder: String, ...)
case selection(...)
case button(title: String, ...)
case facebookButton(...)
case toggle(title: String, ...)
...
}
33
let components: [Component] = [
.facebookButton(...), // Row 0
.text("OR"), // Row 1
.textInput(placeholder: "First name", ...), // Row 2
.textInput(placeholder: "Last name", ...), // Row 3
.textInput(placeholder: "Email", ...), // Row 4
.password(placeholder: "Password", ...), // Row 5
...
]
34
Byusinganorderedcollectionwecan
derive therespective
state (indexpaths)witha
clear and
declarative way 35
Adding, removing or moving
elements is super easy✨
let components: [Component] = [
.textInput(placeholder: "First name", ...), // Row 0
.textInput(placeholder: "Last name", ...), // Row 1
.textInput(placeholder: "Email", ...), // Row 2
.password(placeholder: "Password", ...), // Row 3
...
]
36
Buthow dowe go
fromacollection of
componentstoa
form? !
37
protocol Form {
var components: Property<[FormComponent]> { get }
}
protocol FormComponent {
func matches(with component: FormComponent) -> Bool
func render() -> UIView & FormComponentView
}
protocol FormComponentView {
...
}
enum Component: FormComponent {
...
}
38
⚠ Disclaimer ⚠
The following examplesare
based on MVVM
39
40
Butthis can be
appliedtoanything,
including MVC ifyou
are wondering !
41
class SignUpViewModel: Form {
let components: Property<[Component]>
init(...) {
self.components = Property(value: [
.facebookButton(...),
.text("OR"),
.textInput(placeholder: "First name", ...),
.textInput(placeholder: "Last name", ...),
.textInput(placeholder: "Email", ...),
.password(placeholder: "Password", ...),
...
])
}
}
42
final class FormTableViewDataSource: NSObject, UITableViewDataSource {
private var currentComponents: [FormComponent] = []
init(tableView: UITableView, components: Property<[FormComponent]>) {
components.observe { components in
self.currentComponents = components
tableView.reloadData()
}
}
}
open class FormViewController<F: Form>: UIViewController {
private let tableView = UITableView()
private let dataSource: FormTableViewDataSource
init(form: F) {
dataSource = FormTableViewDataSource(
tableView: tableView,
components: form.components
)
...
}
}
43
final class SignUpViewController: FormViewController {
init(viewModel: SignUpViewModel) {
super.init(form: viewModel)
}
}
44
Ok butwhatifwe needa
dynamic collection of
components? !
45
Quick Example:
Sign-Inwithtwotypes ofauthentication
» Email + Password
» Email + Membership ID
46
47
enum SignInType {
case standard
case membership
}
class SignInViewModel: Form {
let components: Property<[Component]>
init(...) {
self.components = ??? !
}
}
48
switch signInType {
case .standard:
components = Property(value: [
.textInput(placeholder: "Email", ...),
.password(placeholder: "Password", ...),
.toggle(title: "Sign-In with Membership ID", ...)
.button(title: "Submit", ...)
])
case .membership:
components = Property(value: [
.textInput(placeholder: "Email", ...),
.toggle(title: "Sign-In with Membership ID", ...)
.textInput(placeholder: "Membership ID", ...),
.button(title: "Submit", ...)
])
}
49
Firstwe needatriggerto change from one
statetothe other
.toggle(title: "Sign-In with Membership ID", ...)
50
enum Component: FormComponent {
...
case toggle(title: String, isOn: Property<Bool>)
...
}
Nowwe can define component's initialvalue
and observeanychanges
51
enum SignInType {
case standard
case membership
}
class SignInViewModel: Form {
private isMembershipSelected = Property(value: false)
let components: Property<[Component]>
init(...) {
self.components = ??? !
}
}
52
enum SignInType { case standard, membership }
class SignInViewModel: Form {
private isMembershipSelected = Property(value: false)
private signInType: Property<SignInType>
let components: Property<[Component]>
init(...) {
self.signInType = isMembershipSelected.map { isSelected in
return isSelected ? .membership : .standard
}
self.components = ??? !
}
}
53
enum SignInType { case standard, membership }
class SignInViewModel: Form {
private isMembershipSelected = Property(value: false)
private signInType: Property<SignInType>
let components: Property<[Component]>
init(...) {
self.signInType = ...
self.components = signInType.map { signInType in
switch signInType {
case .standard:
...
case .membership:
...
}
}
}
}
54
self.components = signInType.map { signInType in
switch signInType {
case .standard: return [
...
.toggle(
title: "Sign-In with Membership ID",
isOn: isMembershipSelected
)
...
]
case .membership: return [
...
.toggle(
title: "Sign-In with Membership ID",
isOn: isMembershipSelected
)
...
]
}
}
55
56
switch signInType {
case .standard:
components = Property(value: [
.textInput(placeholder: "Email", ...),
...
.toggle(title: "Sign-In with Membership ID", ...)
.button(title: "Submit", ...)
])
case .membership:
components = Property(value: [
.textInput(placeholder: "Email", ...),
.toggle(title: "Sign-In with Membership ID", ...)
...
.button(title: "Submit", ...)
])
}
57
Weare repeatingthe same
components for each
state... can'twe compose
this inabetterway?
58
Wearerepeatingthesamecomponents
foreachstate...can'twe compose
thisinabetterway?
59
FormBuilder
60
struct FormBuilder<Component: FormComponent> {
let components: [Component]
public static var empty: FormBuilder {
return FormBuilder()
}
init(components: [Component] = []) {
self.components = components
}
}
61
We havethe container, now
we onlyneed methodsto
compose
62
Butwhymethodswhenwe
have operators?
63
precedencegroup ChainingPrecedence {
associativity: left
higherThan: TernaryPrecedence
}
// Compose with a new component
infix operator |-+ : ChainingPrecedence
// Compose with another builder
infix operator |-* : ChainingPrecedence
64
struct FormBuilder {
...
static func |-+(
builder: FormBuilder,
component: Component
) -> FormBuilder {
...
}
static func |-* (
builder: FormBuilder,
components: [Component]
) -> FormBuilder {
...
}
}
65
func signInStandardComponents(for signInType: SignInType) -> FormBuilder {
guard let signInType == .standard else { return .empty }
return FormBuilder.empty
|-+ .password(placeholder: "Password", ...)
}
func signInMembershipComponents(for signInType: SignInType) -> FormBuilder {
guard let signInType == .standard else { return .empty }
return FormBuilder.empty
|-+ .textInput(placeholder: "Membership ID", ...)
}
self.components = signInType.map { signInType in
let builder = FormBuilder<Component>.empty
|-+ .textInput(placeholder: "Email", ...)
|-* signInStandardComponents(for: signInType)
|-+ .toggle(title: "Sign-In with Membership ID", isOn: isMembershipSelected)
|-* signInMembershipComponents(for: signInType)
|-+ .button(title: "Submit", ...)
return builder.components
}
66
Composingabuilderwithanother
builder is powerfulbutmaybewe can
have something simpler
67
// Compose components pending on a boolean condition
infix operator |-? : ChainingPrecedence
struct FormBuilder<Component: FormComponent> {
...
static func |-? (
builder: FormBuilder,
validator: Validator<FormBuilder<Component>>
) -> FormBuilder {
...
}
}
struct Validator<T> {
// `iff` stands for "if and only if" from math and logic
static func iff(
_ condition: @autoclosure @escaping () -> Bool,
generator: @escaping (T) -> T
) -> Validator<T> {
...
}
}
68
self.components = signInType.map { signInType in
let builder = FormBuilder<Component>.empty
|-+ .textInput(placeholder: "Email", ...)
|-? .iff(signInType == .standard) {
$0 |-+ .password(placeholder: "Password", ...)
}
|-+ .toggle(title: "Sign-In with Membership ID", isOn: isMembershipSelected)
|-? .iff(signInType == .membership) {
$0 |-+ .textInput(placeholder: "Membership ID", ...)
}
|-+ .button(title: "Submit", ...)
return builder.components
}
69
Nowwe haveadynamic collection of
componentsand consequentlya
dynamic rendering
70
Nowwehaveadynamiccollectionofcomponentsand
consequentlyadynamic rendering !
71
We can definearendererto
renderthe collection of
components for each state
72
protocol Renderer {
associatedtype FormState
func render(state: FormState) -> [FormComponent]
}
struct SignInRenderer: Renderer {
func render(state: SignInType) -> [FormComponent] {
let builder = FormBuilder<Component>.empty
|-+ .textInput(placeholder: "Email", ...)
|-? .iff(signInType == .standard) {
$0 |-+ .password(placeholder: "Password", ...)
}
|-+ .toggle(title: "Sign-In with Membership ID", isOn: isMembershipSelected)
|-? .iff(signInType == .membership) {
$0 |-+ .textInput(placeholder: "Membership ID", ...)
}
|-+ .button(title: "Submit", ...)
return builder.components
}
}
73
enum SignInType { case standard, membership }
class SignInViewModel: Form {
private isMembershipSelected = Property(value: false)
private signInType: Property<SignInType>
let components: Property<[Component]>
init(...) {
self.signInType = isMembershipSelected.map { isSelected in
return isSelected ? .membership : .standard
}
let renderer = SignInRenderer(
isMembershipSelected: isMembershipSelected,
...
)
self.components = signInType.map(renderer.render(state:))
}
}
74
Andwiththis canwe have
fancyanimations? !
75
YES76
final class FormTableViewDataSource: NSObject, UITableViewDataSource {
private var currentComponents: [FormComponent] = []
init(tableView: UITableView, components: Property<[FormComponent]>) {
components.observe { components in
self.currentComponents = components
tableView.reloadData() // !
}
}
}
77
WELL
NOT
YET 78
Everytime our state
changes,anewcollection
ofcomponents is
generatedto reflectthat
particular state
79
1> (signInViewModel.signInType, signInViewModel.components)
(SignInType.standard, [
.textInput(placeholder: "Email", ...),
.password(placeholder: "Password", ...),
.toggle(title: "Sign-In with Membership ID", ...),
.button(title: "Submit", ...)
])
2> (signInViewModel.signInType, signInViewModel.components)
(SignInType.membership, [
.textInput(placeholder: "Email", ...),
.toggle(title: "Sign-In with Membership ID", ...),
.textInput(placeholder: "Membership ID", ...),
.button(title: "Submit", ...)
])
80
1> (signInViewModel.signInType, signInViewModel.components)
// (SignInType.standard, [
// .textInput(placeholder: "Email", ...),
.password(placeholder: "Password", ...),
// .toggle(title: "Sign-In with Membership ID", ...),
// .button(title: "Submit", ...)
//])
2> (signInViewModel.signInType, signInViewModel.components)
// (SignInType.membership, [
// .textInput(placeholder: "Email", ...),
// .toggle(title: "Sign-In with Membership ID", ...),
.textInput(placeholder: "Membership ID", ...),
// .button(title: "Submit", ...)
//])
81
SignInType.standard ➡ SignInType.membership
» REMOVES (-)
.password(placeholder: "Password", ...)
» INSERTS (+)
.textInput(placeholder: "Membership ID", ...)
82
SignInType.membership ➡ SignInType.standard
» REMOVES (-)
.textInput(placeholder: "Membership ID", ...)
» INSERTS (+)
.password(placeholder: "Password", ...)
83
Rememberthis?
protocol FormComponent {
func matches(with component: FormComponent) -> Bool
}
84
Givenacertain component
we can match itagainst
another componentto
validate iftheyare
equivalentor not
85
1> let componentA = Component.password(placeholder: "Password", ...)
2> let componentB = Component.password(placeholder: "Password", ...)
3> let componentC = Component.textInput(placeholder: "Membership ID", ...)
4> componentA.matches(with: componentA)
true
5> componentA.matches(with: componentB)
true
6> componentA.matches(with: componentC)
false
86
Then byemployingadiffing
algorithmwe can identify
anychanges betweentwo
collections ofcomponents
87
import Dwifft
static func diff<Value>(
_ lhs: [Value],
_ rhs: [Value]
) -> [DiffStep<Value>] where Value: Equatable
88
import Dwifft
static func diff<Value>(
_ lhs: [Value],
_ rhs: [Value]
) -> [DiffStep<Value>] where Value: Equatable
enum DiffStep<Value> {
case insert(Int, Value)
case delete(Int, Value)
}
89
import Dwifft
static func diff<Value>(
_ lhs: [Value],
_ rhs: [Value]
) -> [DiffStep<Value>] where Value: Equatable
enum DiffStep<Value> {
case insert(Int, Value)
case delete(Int, Value)
}
Value: Equatable !
90
struct FormNode: Equatable {
let component: FormComponent
static func ==(left: FormNode, right: FormNode) -> Bool {
return left.component.matches(with: right.component)
}
}
91
struct FormBuilder<Component: FormComponent> {
...
func build() -> [FormNode] {
return components.map(FormNode.init)
}
}
protocol Form {
var nodes: Property<[FormNode]> { get }
}
92
final class FormTableViewDataSource: NSObject, UITableViewDataSource {
private var currentNodes: [FormNode] = []
init(tableView: UITableView, nodes: Property<[FormNode]>) {
nodes.observe { nodes in
self.currentNodes = nodes
tableView.reloadData()
}
}
}
93
nodes.observe { nodes in
let previousNodes = self.currentNodes
self.currentNodes = nodes
tableView.beginUpdates()
for step in Dwifft.diff(previousNodes, currentNodes) {
switch step {
case let .insert(index, _):
tableView.insertRows(
at: [IndexPath(row: index, section: 0)], with: .fade)
case let .delete(index, _):
tableView.deleteRows(
at: [IndexPath(row: index, section: 0)], with: .fade)
}
}
tableView.endUpdates()
}
94
Timeto recap
95
96
The conceptis highlyinspired on React
from Facebook
97
⏰ Timeto close ⏰
98
Managing state is hard but
we cantryto minimise how
hard itbecomes
99
State derivation is
essentialto reduce
mutabilityandachievea
self-descriptive system
100
FormsKit's ultimate goalis
to helpcreatingand
managinganykind ofform
Openforextension
101
Open source
January'18102
Thank
You ! 103

FormsKit: reactive forms driven by state. UA Mobile 2017.

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
    Managing state canbean extremelyhardtask » Mutability » Observation » Thread-safety 5
  • 6.
  • 7.
  • 8.
    final class Property<Value>{ private var handlers: [(Value) -> Void] = [] var value: Value { didSet { handlers.forEach { $0(value) } } } init(value: Value) { self.value = value } func observe(_ handler: @escaping (Value) -> Void) { self.handlers.append(handler) handler(value) } func map<T>(_ transform: @escaping (Value) -> T) -> Property<T> { let property = Property<T>(value: transform(value)) observe { [weak property] value in property?.value = transform(value) } return property } } 8
  • 9.
    ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠ Please notethis isafairly simpleimplementation ! ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠ 9
  • 10.
    Propertygive usanicewayto observe changes 1>let a = Property(value: 1) 2> a.observe { value in print("Value: (value)") } "Value: 1" 3> a.value = a.value + 2 "Value: 3" 10
  • 11.
    Andto derive newstates 1>let a = Property(value: 1.0) 2> let b = a.map { value in "(value) as String" } 4> b.value "1.0 as String" 5> a.value = a.value * 10 6> a.value 10.0 7> b.value "10.0 as String" 11
  • 12.
  • 13.
  • 14.
    // Sign-Up TableView DataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 12 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { switch indexPath.row { case 0: // Facebook button case 1: // Or text case 2: // First Name case 3: // Last Name ... } } 14
  • 15.
    Adding or removingelements requires: » update the total number of elements » shift all indices affected Moving elements requires: » shift all indices affected 15
  • 16.
  • 17.
  • 18.
    func tableView(_ tableView:UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { switch indexPath.row { case 0: // Account selected case 1: switch consultantType { case .gp: switch dateAndTimeAvailable { case true: ... case false: ... } case .specialist: ... } case ???: ... ... } } 18
  • 19.
  • 20.
  • 21.
    Index pathsare aweak system, hardto manage and maintain21
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
    Aform isacollection of componentsfollowing asequential order 26
  • 27.
  • 28.
    A form isacollection ofcomponents following a sequentialorder 28
  • 29.
  • 30.
    ArrayAn ordered, random-accesscollection 1> let array = [0, 1, 2] 2> array [ 0, 1, 2 ] 30
  • 31.
  • 32.
    » Text » TextInput » Password » Selection » Buttons » ... 32
  • 33.
    enum Component { casetext(String) case textInput(placeholder: String, ...) case password(placeholder: String, ...) case selection(...) case button(title: String, ...) case facebookButton(...) case toggle(title: String, ...) ... } 33
  • 34.
    let components: [Component]= [ .facebookButton(...), // Row 0 .text("OR"), // Row 1 .textInput(placeholder: "First name", ...), // Row 2 .textInput(placeholder: "Last name", ...), // Row 3 .textInput(placeholder: "Email", ...), // Row 4 .password(placeholder: "Password", ...), // Row 5 ... ] 34
  • 35.
  • 36.
    Adding, removing ormoving elements is super easy✨ let components: [Component] = [ .textInput(placeholder: "First name", ...), // Row 0 .textInput(placeholder: "Last name", ...), // Row 1 .textInput(placeholder: "Email", ...), // Row 2 .password(placeholder: "Password", ...), // Row 3 ... ] 36
  • 37.
    Buthow dowe go fromacollectionof componentstoa form? ! 37
  • 38.
    protocol Form { varcomponents: Property<[FormComponent]> { get } } protocol FormComponent { func matches(with component: FormComponent) -> Bool func render() -> UIView & FormComponentView } protocol FormComponentView { ... } enum Component: FormComponent { ... } 38
  • 39.
    ⚠ Disclaimer ⚠ Thefollowing examplesare based on MVVM 39
  • 40.
  • 41.
    Butthis can be appliedtoanything, includingMVC ifyou are wondering ! 41
  • 42.
    class SignUpViewModel: Form{ let components: Property<[Component]> init(...) { self.components = Property(value: [ .facebookButton(...), .text("OR"), .textInput(placeholder: "First name", ...), .textInput(placeholder: "Last name", ...), .textInput(placeholder: "Email", ...), .password(placeholder: "Password", ...), ... ]) } } 42
  • 43.
    final class FormTableViewDataSource:NSObject, UITableViewDataSource { private var currentComponents: [FormComponent] = [] init(tableView: UITableView, components: Property<[FormComponent]>) { components.observe { components in self.currentComponents = components tableView.reloadData() } } } open class FormViewController<F: Form>: UIViewController { private let tableView = UITableView() private let dataSource: FormTableViewDataSource init(form: F) { dataSource = FormTableViewDataSource( tableView: tableView, components: form.components ) ... } } 43
  • 44.
    final class SignUpViewController:FormViewController { init(viewModel: SignUpViewModel) { super.init(form: viewModel) } } 44
  • 45.
    Ok butwhatifwe needa dynamiccollection of components? ! 45
  • 46.
    Quick Example: Sign-Inwithtwotypes ofauthentication »Email + Password » Email + Membership ID 46
  • 47.
  • 48.
    enum SignInType { casestandard case membership } class SignInViewModel: Form { let components: Property<[Component]> init(...) { self.components = ??? ! } } 48
  • 49.
    switch signInType { case.standard: components = Property(value: [ .textInput(placeholder: "Email", ...), .password(placeholder: "Password", ...), .toggle(title: "Sign-In with Membership ID", ...) .button(title: "Submit", ...) ]) case .membership: components = Property(value: [ .textInput(placeholder: "Email", ...), .toggle(title: "Sign-In with Membership ID", ...) .textInput(placeholder: "Membership ID", ...), .button(title: "Submit", ...) ]) } 49
  • 50.
    Firstwe needatriggerto changefrom one statetothe other .toggle(title: "Sign-In with Membership ID", ...) 50
  • 51.
    enum Component: FormComponent{ ... case toggle(title: String, isOn: Property<Bool>) ... } Nowwe can define component's initialvalue and observeanychanges 51
  • 52.
    enum SignInType { casestandard case membership } class SignInViewModel: Form { private isMembershipSelected = Property(value: false) let components: Property<[Component]> init(...) { self.components = ??? ! } } 52
  • 53.
    enum SignInType {case standard, membership } class SignInViewModel: Form { private isMembershipSelected = Property(value: false) private signInType: Property<SignInType> let components: Property<[Component]> init(...) { self.signInType = isMembershipSelected.map { isSelected in return isSelected ? .membership : .standard } self.components = ??? ! } } 53
  • 54.
    enum SignInType {case standard, membership } class SignInViewModel: Form { private isMembershipSelected = Property(value: false) private signInType: Property<SignInType> let components: Property<[Component]> init(...) { self.signInType = ... self.components = signInType.map { signInType in switch signInType { case .standard: ... case .membership: ... } } } } 54
  • 55.
    self.components = signInType.map{ signInType in switch signInType { case .standard: return [ ... .toggle( title: "Sign-In with Membership ID", isOn: isMembershipSelected ) ... ] case .membership: return [ ... .toggle( title: "Sign-In with Membership ID", isOn: isMembershipSelected ) ... ] } } 55
  • 56.
  • 57.
    switch signInType { case.standard: components = Property(value: [ .textInput(placeholder: "Email", ...), ... .toggle(title: "Sign-In with Membership ID", ...) .button(title: "Submit", ...) ]) case .membership: components = Property(value: [ .textInput(placeholder: "Email", ...), .toggle(title: "Sign-In with Membership ID", ...) ... .button(title: "Submit", ...) ]) } 57
  • 58.
    Weare repeatingthe same componentsfor each state... can'twe compose this inabetterway? 58
  • 59.
  • 60.
  • 61.
    struct FormBuilder<Component: FormComponent>{ let components: [Component] public static var empty: FormBuilder { return FormBuilder() } init(components: [Component] = []) { self.components = components } } 61
  • 62.
    We havethe container,now we onlyneed methodsto compose 62
  • 63.
  • 64.
    precedencegroup ChainingPrecedence { associativity:left higherThan: TernaryPrecedence } // Compose with a new component infix operator |-+ : ChainingPrecedence // Compose with another builder infix operator |-* : ChainingPrecedence 64
  • 65.
    struct FormBuilder { ... staticfunc |-+( builder: FormBuilder, component: Component ) -> FormBuilder { ... } static func |-* ( builder: FormBuilder, components: [Component] ) -> FormBuilder { ... } } 65
  • 66.
    func signInStandardComponents(for signInType:SignInType) -> FormBuilder { guard let signInType == .standard else { return .empty } return FormBuilder.empty |-+ .password(placeholder: "Password", ...) } func signInMembershipComponents(for signInType: SignInType) -> FormBuilder { guard let signInType == .standard else { return .empty } return FormBuilder.empty |-+ .textInput(placeholder: "Membership ID", ...) } self.components = signInType.map { signInType in let builder = FormBuilder<Component>.empty |-+ .textInput(placeholder: "Email", ...) |-* signInStandardComponents(for: signInType) |-+ .toggle(title: "Sign-In with Membership ID", isOn: isMembershipSelected) |-* signInMembershipComponents(for: signInType) |-+ .button(title: "Submit", ...) return builder.components } 66
  • 67.
  • 68.
    // Compose componentspending on a boolean condition infix operator |-? : ChainingPrecedence struct FormBuilder<Component: FormComponent> { ... static func |-? ( builder: FormBuilder, validator: Validator<FormBuilder<Component>> ) -> FormBuilder { ... } } struct Validator<T> { // `iff` stands for "if and only if" from math and logic static func iff( _ condition: @autoclosure @escaping () -> Bool, generator: @escaping (T) -> T ) -> Validator<T> { ... } } 68
  • 69.
    self.components = signInType.map{ signInType in let builder = FormBuilder<Component>.empty |-+ .textInput(placeholder: "Email", ...) |-? .iff(signInType == .standard) { $0 |-+ .password(placeholder: "Password", ...) } |-+ .toggle(title: "Sign-In with Membership ID", isOn: isMembershipSelected) |-? .iff(signInType == .membership) { $0 |-+ .textInput(placeholder: "Membership ID", ...) } |-+ .button(title: "Submit", ...) return builder.components } 69
  • 70.
    Nowwe haveadynamic collectionof componentsand consequentlya dynamic rendering 70
  • 71.
  • 72.
    We can definearendererto renderthecollection of components for each state 72
  • 73.
    protocol Renderer { associatedtypeFormState func render(state: FormState) -> [FormComponent] } struct SignInRenderer: Renderer { func render(state: SignInType) -> [FormComponent] { let builder = FormBuilder<Component>.empty |-+ .textInput(placeholder: "Email", ...) |-? .iff(signInType == .standard) { $0 |-+ .password(placeholder: "Password", ...) } |-+ .toggle(title: "Sign-In with Membership ID", isOn: isMembershipSelected) |-? .iff(signInType == .membership) { $0 |-+ .textInput(placeholder: "Membership ID", ...) } |-+ .button(title: "Submit", ...) return builder.components } } 73
  • 74.
    enum SignInType {case standard, membership } class SignInViewModel: Form { private isMembershipSelected = Property(value: false) private signInType: Property<SignInType> let components: Property<[Component]> init(...) { self.signInType = isMembershipSelected.map { isSelected in return isSelected ? .membership : .standard } let renderer = SignInRenderer( isMembershipSelected: isMembershipSelected, ... ) self.components = signInType.map(renderer.render(state:)) } } 74
  • 75.
  • 76.
  • 77.
    final class FormTableViewDataSource:NSObject, UITableViewDataSource { private var currentComponents: [FormComponent] = [] init(tableView: UITableView, components: Property<[FormComponent]>) { components.observe { components in self.currentComponents = components tableView.reloadData() // ! } } } 77
  • 78.
  • 79.
    Everytime our state changes,anewcollection ofcomponentsis generatedto reflectthat particular state 79
  • 80.
    1> (signInViewModel.signInType, signInViewModel.components) (SignInType.standard,[ .textInput(placeholder: "Email", ...), .password(placeholder: "Password", ...), .toggle(title: "Sign-In with Membership ID", ...), .button(title: "Submit", ...) ]) 2> (signInViewModel.signInType, signInViewModel.components) (SignInType.membership, [ .textInput(placeholder: "Email", ...), .toggle(title: "Sign-In with Membership ID", ...), .textInput(placeholder: "Membership ID", ...), .button(title: "Submit", ...) ]) 80
  • 81.
    1> (signInViewModel.signInType, signInViewModel.components) //(SignInType.standard, [ // .textInput(placeholder: "Email", ...), .password(placeholder: "Password", ...), // .toggle(title: "Sign-In with Membership ID", ...), // .button(title: "Submit", ...) //]) 2> (signInViewModel.signInType, signInViewModel.components) // (SignInType.membership, [ // .textInput(placeholder: "Email", ...), // .toggle(title: "Sign-In with Membership ID", ...), .textInput(placeholder: "Membership ID", ...), // .button(title: "Submit", ...) //]) 81
  • 82.
    SignInType.standard ➡ SignInType.membership »REMOVES (-) .password(placeholder: "Password", ...) » INSERTS (+) .textInput(placeholder: "Membership ID", ...) 82
  • 83.
    SignInType.membership ➡ SignInType.standard »REMOVES (-) .textInput(placeholder: "Membership ID", ...) » INSERTS (+) .password(placeholder: "Password", ...) 83
  • 84.
    Rememberthis? protocol FormComponent { funcmatches(with component: FormComponent) -> Bool } 84
  • 85.
    Givenacertain component we canmatch itagainst another componentto validate iftheyare equivalentor not 85
  • 86.
    1> let componentA= Component.password(placeholder: "Password", ...) 2> let componentB = Component.password(placeholder: "Password", ...) 3> let componentC = Component.textInput(placeholder: "Membership ID", ...) 4> componentA.matches(with: componentA) true 5> componentA.matches(with: componentB) true 6> componentA.matches(with: componentC) false 86
  • 87.
    Then byemployingadiffing algorithmwe canidentify anychanges betweentwo collections ofcomponents 87
  • 88.
    import Dwifft static funcdiff<Value>( _ lhs: [Value], _ rhs: [Value] ) -> [DiffStep<Value>] where Value: Equatable 88
  • 89.
    import Dwifft static funcdiff<Value>( _ lhs: [Value], _ rhs: [Value] ) -> [DiffStep<Value>] where Value: Equatable enum DiffStep<Value> { case insert(Int, Value) case delete(Int, Value) } 89
  • 90.
    import Dwifft static funcdiff<Value>( _ lhs: [Value], _ rhs: [Value] ) -> [DiffStep<Value>] where Value: Equatable enum DiffStep<Value> { case insert(Int, Value) case delete(Int, Value) } Value: Equatable ! 90
  • 91.
    struct FormNode: Equatable{ let component: FormComponent static func ==(left: FormNode, right: FormNode) -> Bool { return left.component.matches(with: right.component) } } 91
  • 92.
    struct FormBuilder<Component: FormComponent>{ ... func build() -> [FormNode] { return components.map(FormNode.init) } } protocol Form { var nodes: Property<[FormNode]> { get } } 92
  • 93.
    final class FormTableViewDataSource:NSObject, UITableViewDataSource { private var currentNodes: [FormNode] = [] init(tableView: UITableView, nodes: Property<[FormNode]>) { nodes.observe { nodes in self.currentNodes = nodes tableView.reloadData() } } } 93
  • 94.
    nodes.observe { nodesin let previousNodes = self.currentNodes self.currentNodes = nodes tableView.beginUpdates() for step in Dwifft.diff(previousNodes, currentNodes) { switch step { case let .insert(index, _): tableView.insertRows( at: [IndexPath(row: index, section: 0)], with: .fade) case let .delete(index, _): tableView.deleteRows( at: [IndexPath(row: index, section: 0)], with: .fade) } } tableView.endUpdates() } 94
  • 95.
  • 96.
  • 97.
    The conceptis highlyinspiredon React from Facebook 97
  • 98.
  • 99.
    Managing state ishard but we cantryto minimise how hard itbecomes 99
  • 100.
    State derivation is essentialtoreduce mutabilityandachievea self-descriptive system 100
  • 101.
    FormsKit's ultimate goalis tohelpcreatingand managinganykind ofform Openforextension 101
  • 102.
  • 103.