SlideShare a Scribd company logo
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

More Related Content

PDF
Taming forms with React
GreeceJS
 
PPTX
Operator overload rr
Dhivya Shanmugam
 
PPTX
The zen of async: Best practices for best performance
Microsoft Developer Network (MSDN) - Belgium and Luxembourg
 
KEY
Symfony2 Building on Alpha / Beta technology
Daniel Knell
 
PDF
Specs2
Piyush Mishra
 
PDF
Юрий Буянов «Squeryl — ORM с человеческим лицом»
e-Legion
 
PPT
2 introduction toentitybeans
ashishkirpan
 
TXT
Data20161007
capegmail
 
Taming forms with React
GreeceJS
 
Operator overload rr
Dhivya Shanmugam
 
The zen of async: Best practices for best performance
Microsoft Developer Network (MSDN) - Belgium and Luxembourg
 
Symfony2 Building on Alpha / Beta technology
Daniel Knell
 
Юрий Буянов «Squeryl — ORM с человеческим лицом»
e-Legion
 
2 introduction toentitybeans
ashishkirpan
 
Data20161007
capegmail
 

What's hot (20)

PDF
Async code on kotlin: rx java or/and coroutines - Kotlin Night Turin
Fabio Collini
 
PDF
Symfony CoP: Form component
Samuel ROZE
 
PDF
Architectures in the compose world
Fabio Collini
 
PDF
SISTEMA DE FACTURACION (Ejemplo desarrollado)
Darwin Durand
 
PDF
Better Bullshit Driven Development [SeleniumCamp 2017]
automician
 
PDF
How I started to love design patterns
Samuel ROZE
 
PDF
Dig Deeper into WordPress - WD Meetup Cairo
Mohamed Mosaad
 
KEY
Why ruby
rstankov
 
PDF
Kotlin delegates in practice - Kotlin Everywhere Stockholm
Fabio Collini
 
PDF
From Java to Kotlin beyond alt+shift+cmd+k - Kotlin Community Conf Milan
Fabio Collini
 
PPTX
Hacking Your Way To Better Security - Dutch PHP Conference 2016
Colin O'Dell
 
PDF
The Ring programming language version 1.5.4 book - Part 44 of 185
Mahmoud Samir Fayed
 
KEY
Ruby/Rails
rstankov
 
PDF
Tom Lazar Using Zope3 Views And Viewlets For Plone 3.0 Product Development
Vincenzo Barone
 
PDF
How to Mess Up Your Angular UI Components
cagataycivici
 
KEY
Backbone js
rstankov
 
PDF
Chaining and function composition with lodash / underscore
Nicolas Carlo
 
PDF
The art of reverse engineering flash exploits
Priyanka Aash
 
PDF
Kotlin Delegates in practice - Kotlin community conf
Fabio Collini
 
PDF
Unbreakable: The Craft of Code
Joe Morgan
 
Async code on kotlin: rx java or/and coroutines - Kotlin Night Turin
Fabio Collini
 
Symfony CoP: Form component
Samuel ROZE
 
Architectures in the compose world
Fabio Collini
 
SISTEMA DE FACTURACION (Ejemplo desarrollado)
Darwin Durand
 
Better Bullshit Driven Development [SeleniumCamp 2017]
automician
 
How I started to love design patterns
Samuel ROZE
 
Dig Deeper into WordPress - WD Meetup Cairo
Mohamed Mosaad
 
Why ruby
rstankov
 
Kotlin delegates in practice - Kotlin Everywhere Stockholm
Fabio Collini
 
From Java to Kotlin beyond alt+shift+cmd+k - Kotlin Community Conf Milan
Fabio Collini
 
Hacking Your Way To Better Security - Dutch PHP Conference 2016
Colin O'Dell
 
The Ring programming language version 1.5.4 book - Part 44 of 185
Mahmoud Samir Fayed
 
Ruby/Rails
rstankov
 
Tom Lazar Using Zope3 Views And Viewlets For Plone 3.0 Product Development
Vincenzo Barone
 
How to Mess Up Your Angular UI Components
cagataycivici
 
Backbone js
rstankov
 
Chaining and function composition with lodash / underscore
Nicolas Carlo
 
The art of reverse engineering flash exploits
Priyanka Aash
 
Kotlin Delegates in practice - Kotlin community conf
Fabio Collini
 
Unbreakable: The Craft of Code
Joe Morgan
 
Ad

Similar to FormsKit: reactive forms driven by state. UA Mobile 2017. (20)

PDF
Advanced functional programing in Swift
Vincent Pradeilles
 
KEY
RAD CRUD
Eugene Lazutkin
 
PDF
An introduction to functional programming with Swift
Fatih Nayebi, Ph.D.
 
PPTX
Swift as an OOP Language
Ahmed Ali
 
PPTX
Unit 2 - Introduction to UIKit
Franco Cedillo
 
PDF
Eureka wtb presentation - Mathias Classen
WithTheBest
 
PDF
Functional Reactive Programming - RxSwift
Rodrigo Leite
 
PDF
Quick swift tour
Kazunobu Tasaka
 
PPTX
Functional DDD
Alessandro Melchiori
 
PDF
Антон Молдован "Type driven development with f#"
Fwdays
 
PDF
Introduction to Swift 2
Joris Timmerman
 
PDF
Swift - the future of iOS app development
openak
 
PDF
Swift Introduction
Giuseppe Arici
 
PDF
Swift Programming
Codemotion
 
PPTX
Basic iOS Training with SWIFT - Part 3
Manoj Ellappan
 
PPTX
iOS development using Swift - Swift Basics (2)
Ahmed Ali
 
PDF
Swift Study #2
chanju Jeon
 
PPTX
Functional GUIs with F#
Frank Krueger
 
PDF
Think sharp, write swift
Pascal Batty
 
PDF
7 Habits For a More Functional Swift
Jason Larsen
 
Advanced functional programing in Swift
Vincent Pradeilles
 
RAD CRUD
Eugene Lazutkin
 
An introduction to functional programming with Swift
Fatih Nayebi, Ph.D.
 
Swift as an OOP Language
Ahmed Ali
 
Unit 2 - Introduction to UIKit
Franco Cedillo
 
Eureka wtb presentation - Mathias Classen
WithTheBest
 
Functional Reactive Programming - RxSwift
Rodrigo Leite
 
Quick swift tour
Kazunobu Tasaka
 
Functional DDD
Alessandro Melchiori
 
Антон Молдован "Type driven development with f#"
Fwdays
 
Introduction to Swift 2
Joris Timmerman
 
Swift - the future of iOS app development
openak
 
Swift Introduction
Giuseppe Arici
 
Swift Programming
Codemotion
 
Basic iOS Training with SWIFT - Part 3
Manoj Ellappan
 
iOS development using Swift - Swift Basics (2)
Ahmed Ali
 
Swift Study #2
chanju Jeon
 
Functional GUIs with F#
Frank Krueger
 
Think sharp, write swift
Pascal Batty
 
7 Habits For a More Functional Swift
Jason Larsen
 
Ad

More from UA Mobile (20)

PDF
Designing iOS+Android project without using multiplatform frameworks - UA Mob...
UA Mobile
 
PDF
Декларативное программирование клиент-серверных приложений на андроид - UA Mo...
UA Mobile
 
PDF
Leave your Room behind - UA Mobile 2019
UA Mobile
 
PDF
OpenId and OAuth2: Rear, Medium, Well Done - UA Mobile 2019
UA Mobile
 
PDF
Google Wear OS watch faces and applications development - UA Mobile 2019
UA Mobile
 
PDF
Історія декількох проектів та що в них пішло не так - UA Mobile 2019
UA Mobile
 
PDF
Working effectively with ViewModels and TDD - UA Mobile 2019
UA Mobile
 
PDF
Managing State in Reactive applications - UA Mobile 2019
UA Mobile
 
PDF
Ідіоматична ін'єкція залежностей на Kotlin без фреймворків - UA Mobile2019
UA Mobile
 
PDF
Актуальні практики дизайну мобільних додатків - UA Mobile 2019
UA Mobile
 
PDF
До чого прикладати Docker в Android? - UA Mobile 2019
UA Mobile
 
PDF
Building your Flutter apps using Redux - UA Mobile 2019
UA Mobile
 
PDF
Optional. Tips and Tricks - UA Mobile 2019
UA Mobile
 
PDF
Designing iOS+Android project without using multiplatform frameworks - UA Mob...
UA Mobile
 
PDF
Бібліотеки та Інструменти на сторожі коду - UA Mobile 2019
UA Mobile
 
PDF
Flutter: No more boring apps! - UA Mobile 2019
UA Mobile
 
PDF
Долаючи прірву між дизайнерами та розробниками - UA Mobile 2019
UA Mobile
 
PDF
Multiplatform shared codebase with Kotlin/Native - UA Mobile 2019
UA Mobile
 
PDF
Sceneform SDK на практиці - UA Mobile 2019
UA Mobile
 
PDF
Coroutines in Kotlin. UA Mobile 2017.
UA Mobile
 
Designing iOS+Android project without using multiplatform frameworks - UA Mob...
UA Mobile
 
Декларативное программирование клиент-серверных приложений на андроид - UA Mo...
UA Mobile
 
Leave your Room behind - UA Mobile 2019
UA Mobile
 
OpenId and OAuth2: Rear, Medium, Well Done - UA Mobile 2019
UA Mobile
 
Google Wear OS watch faces and applications development - UA Mobile 2019
UA Mobile
 
Історія декількох проектів та що в них пішло не так - UA Mobile 2019
UA Mobile
 
Working effectively with ViewModels and TDD - UA Mobile 2019
UA Mobile
 
Managing State in Reactive applications - UA Mobile 2019
UA Mobile
 
Ідіоматична ін'єкція залежностей на Kotlin без фреймворків - UA Mobile2019
UA Mobile
 
Актуальні практики дизайну мобільних додатків - UA Mobile 2019
UA Mobile
 
До чого прикладати Docker в Android? - UA Mobile 2019
UA Mobile
 
Building your Flutter apps using Redux - UA Mobile 2019
UA Mobile
 
Optional. Tips and Tricks - UA Mobile 2019
UA Mobile
 
Designing iOS+Android project without using multiplatform frameworks - UA Mob...
UA Mobile
 
Бібліотеки та Інструменти на сторожі коду - UA Mobile 2019
UA Mobile
 
Flutter: No more boring apps! - UA Mobile 2019
UA Mobile
 
Долаючи прірву між дизайнерами та розробниками - UA Mobile 2019
UA Mobile
 
Multiplatform shared codebase with Kotlin/Native - UA Mobile 2019
UA Mobile
 
Sceneform SDK на практиці - UA Mobile 2019
UA Mobile
 
Coroutines in Kotlin. UA Mobile 2017.
UA Mobile
 

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

  • 3. 3
  • 5. Managing state can bean extremelyhardtask » Mutability » Observation » Thread-safety 5
  • 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 simple implementation ! ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠ 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
  • 13. 13
  • 14. // 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
  • 15. Adding or removing elements requires: » update the total number of elements » shift all indices affected Moving elements requires: » shift all indices affected 15
  • 17. 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
  • 20. 20
  • 21. Index pathsare aweak system, hard to manage and maintain21
  • 26. Aform isacollection of components following asequential order 26
  • 27. 27
  • 28. A form isa collection ofcomponents following a sequentialorder 28
  • 30. ArrayAn ordered, random-access collection 1> let array = [0, 1, 2] 2> array [ 0, 1, 2 ] 30
  • 32. » Text » Text Input » Password » Selection » Buttons » ... 32
  • 33. 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
  • 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
  • 36. 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
  • 37. Buthow dowe go fromacollection of componentstoa form? ! 37
  • 38. 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
  • 39. ⚠ Disclaimer ⚠ The following examplesare based on MVVM 39
  • 40. 40
  • 41. Butthis can be appliedtoanything, including MVC 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 dynamic collection of components? ! 45
  • 46. Quick Example: Sign-Inwithtwotypes ofauthentication » Email + Password » Email + Membership ID 46
  • 47. 47
  • 48. enum SignInType { case standard 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 change from 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 { case standard 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. 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 components for each state... can'twe compose this inabetterway? 58
  • 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
  • 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 { ... static func |-+( 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
  • 68. // 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
  • 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 collection of componentsand consequentlya dynamic rendering 70
  • 72. We can definearendererto renderthe collection of components for each state 72
  • 73. 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
  • 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
  • 76. YES76
  • 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
  • 79. Everytime our state changes,anewcollection ofcomponents is 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 { func matches(with component: FormComponent) -> Bool } 84
  • 85. Givenacertain component we can match 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 can identify anychanges betweentwo collections ofcomponents 87
  • 88. import Dwifft static func diff<Value>( _ lhs: [Value], _ rhs: [Value] ) -> [DiffStep<Value>] where Value: Equatable 88
  • 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) } 89
  • 90. 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
  • 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 { 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
  • 96. 96
  • 97. The conceptis highlyinspired on React from Facebook 97
  • 99. Managing state is hard but we cantryto minimise how hard itbecomes 99
  • 100. State derivation is essentialto reduce mutabilityandachievea self-descriptive system 100
  • 101. FormsKit's ultimate goalis to helpcreatingand managinganykind ofform Openforextension 101