Exploring Stateless UIs in Swift

Exploring Stateless UIs in Swift

Taking inspiration from architectural patterns like Flux and Redux, this session is an exploration of one-way data flow principles in Swift.

It's hard to resist the influence of Apple's Cocoa MVC architecture on our apps. We've all seen or written view controllers with thousands of lines of code that buckle under the weight of their responsibilities. They're difficult to test, modify, and maintain.

By thinking differently about how we handle application state over time, we've reduced complexity and improved extensibility of production code. And you don't need to buy into a specific architecture–you can benefit from using one-way data flow principles in your apps right now.

E5703b43507ace0a59aaa59e053d3e61?s=128

Sam Kirchmeier

April 23, 2016
Tweet

Transcript

  1. EXPLORING STATELESS UIs in SWIFT Adam May & Sam Kirchmeier

  2. None
  3. tchacknight.com

  4. TOPICS

  5. Define application state TOPICS

  6. Define application state Describe a stateless architecture for iOS TOPICS

  7. Define application state Describe a stateless architecture for iOS Show

    examples TOPICS
  8. Define application state Describe a stateless architecture for iOS Show

    examples Discuss challenges and future work TOPICS
  9. STATELESS UIs

  10. WHAT IS STATE?

  11. The value of an identity at a moment in time

    WHAT IS STATE? Source: http://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hickey
  12. The value of an identity at a moment in time

    WHAT IS STATE?
  13. The value of an identity at a moment in time

    WHAT IS STATE?
  14. The value of an identity at a moment in time

    WHAT IS STATE?
  15. The value of an identity at a moment in time

    WHAT IS STATE?
  16. STATE IN SWIFT

  17. STATE IN SWIFT var str = "Hello, playground"

  18. Identity Value Time STATE IN SWIFT var str = "Hello,

    playground"
  19. Identity Value Time STATE IN SWIFT var str = "Hello,

    playground"
  20. Identity Value Time STATE IN SWIFT var str = "Hello,

    playground"
  21. HIDDEN COMPLEXITY var str = "Hello, playground"

  22. Assume str is global HIDDEN COMPLEXITY var str = "Hello,

    playground"
  23. Assume str is global It’s mutable from anywhere HIDDEN COMPLEXITY

    var str = "Hello, playground"
  24. ENCAPSULATION class NeatlyEncapsulated { private } var str = "Hello,

    playground"
  25. Mutable via private methods ENCAPSULATION var str = "Hello, playground"

    class NeatlyEncapsulated { private }
  26. Mutable via private methods Indirectly mutable via public methods ENCAPSULATION

    var str = "Hello, playground" class NeatlyEncapsulated { private }
  27. Mutable via private methods Indirectly mutable via public methods Same

    number of moving parts ENCAPSULATION var str = "Hello, playground" class NeatlyEncapsulated { private }
  28. STATE, THE PROBLEM

  29. State breeds complexity STATE, THE PROBLEM

  30. State breeds complexity Unexpected mutations STATE, THE PROBLEM

  31. State breeds complexity Unexpected mutations Concurrency and parallelism STATE, THE

    PROBLEM
  32. State breeds complexity Unexpected mutations Concurrency and parallelism Testing STATE,

    THE PROBLEM
  33. WHAT DOES IT MEAN TO BE STATELESS?

  34. Does not depend on variables WHAT DOES IT MEAN TO

    BE STATELESS?
  35. Does not depend on variables Same inputs same outputs WHAT

    DOES IT MEAN TO BE STATELESS?
  36. Does not depend on variables Same inputs same outputs No

    side effects WHAT DOES IT MEAN TO BE STATELESS?
  37. iOS IS INHERENTLY STATEFUL

  38. Persistence iOS IS INHERENTLY STATEFUL

  39. Persistence Networking iOS IS INHERENTLY STATEFUL

  40. Persistence Networking Memory iOS IS INHERENTLY STATEFUL

  41. Persistence Networking Memory UI iOS IS INHERENTLY STATEFUL

  42. Model View Controller update update notify action Source: https://developer.apple.com/library/ios/documentation/General/ Conceptual/DevPedia-CocoaCore/MVC.html

  43. Model View Controller update update notify action state

  44. Model View Controller update update notify action state state

  45. Model View Controller update update notify action state state state

  46. HOW CAN WE DEAL WITH THIS?

  47. Isolate or eliminate state (vars) HOW CAN WE DEAL WITH

    THIS?
  48. Isolate or eliminate state (vars) Rely on values whenever possible

    HOW CAN WE DEAL WITH THIS?
  49. SWIFT TO THE RESCUE

  50. Constants SWIFT TO THE RESCUE

  51. Constants Value types SWIFT TO THE RESCUE

  52. CONSTANTS let str = "Hello, playground" str = "Goodbye!" //

    Compiler error!
  53. Code that relies on constants is stateless CONSTANTS let str

    = "Hello, playground" str = "Goodbye!" // Compiler error!
  54. VALUE TYPES

  55. Int, Float, Double VALUE TYPES

  56. Int, Float, Double String VALUE TYPES

  57. Int, Float, Double String Array, Dictionary, Set VALUE TYPES

  58. Int, Float, Double String Array, Dictionary, Set Struct, Enum VALUE

    TYPES
  59. Int, Float, Double String Array, Dictionary, Set Struct, Enum Code

    that relies on value types is stateless VALUE TYPES
  60. VALUE TYPES class Product { var price: String }

  61. VALUE TYPES class Product { var price: String } var

    credenza = Product(price: "$11.00")
  62. VALUE TYPES class Product { var price: String } var

    credenza = Product(price: "$11.00") var niceCredenza = credenza niceCredenza.price = "$12.00"
  63. VALUE TYPES class Product { var price: String } var

    credenza = Product(price: "$11.00") var niceCredenza = credenza niceCredenza.price = "$12.00" credenza.price // “$12.00”
  64. VALUE TYPES struct Product { var price: String } class

    Product { var price: String }
  65. VALUE TYPES struct Product { var price: String } var

    credenza = Product(price: "$11.00") var niceCredenza = credenza niceCredenza.price = "$12.00" credenza.price // "$11.00"
  66. WE NEED A NEW PARADIGM

  67. Eliminates and isolates state WE NEED A NEW PARADIGM

  68. Eliminates and isolates state Takes advantage of Swift’s constants WE

    NEED A NEW PARADIGM
  69. Eliminates and isolates state Takes advantage of Swift’s constants Takes

    advantage of Swift’s value types WE NEED A NEW PARADIGM
  70. View Store Dispatcher

  71. View Store Dispatcher

  72. Model Controller View updates manipulates User uses shows Source: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller

  73. View Store Dispatcher

  74. View Store

  75. View Store Stores the current state state

  76. View Store Stores the current state Produces the next state

    state
  77. View Store Stores the current state Produces the next state

    Is a class state
  78. View Store Dispatcher state

  79. View Store state

  80. View Store state Is app state

  81. View Store state Is app state (everything)

  82. View Store state Is app state (everything) Is a moment

    in time
  83. View Store state Is app state (everything) Is a moment

    in time Is immutable
  84. View Store state Is app state (everything) Is a moment

    in time Is immutable Is a value
  85. View Store Dispatcher state

  86. View Store Dispatcher state state

  87. View

  88. View Receives and renders state

  89. View Receives and renders state Can’t mutate state

  90. View Receives and renders state Can’t mutate state Triggers actions

  91. View Receives and renders state Can’t mutate state Triggers actions

    Is a view controller or view
  92. View Receives and renders state Can’t mutate state Triggers actions

    Is a view controller or view Is a class
  93. View Store Dispatcher state state

  94. View Store Dispatcher state state action

  95. View action

  96. View action Represents an interaction

  97. View action Represents an interaction Is not state

  98. View action Represents an interaction Is not state Includes an

    action type
  99. View action Represents an interaction Is not state Includes an

    action type Includes relevant data
  100. View action Represents an interaction Is not state Includes an

    action type Includes relevant data Is a value
  101. View Store Dispatcher state action state

  102. View Dispatcher state action

  103. View Dispatcher state action Receives actions

  104. View Dispatcher state action Receives actions Forwards to subscribers

  105. View Dispatcher state action Receives actions Forwards to subscribers Has

    multiple subscribers
  106. View Dispatcher state action Receives actions Forwards to subscribers Has

    multiple subscribers Is a class
  107. View Store Dispatcher state action state

  108. View Store Dispatcher state action action state Store state

  109. Store state

  110. Store action state

  111. Store action + current state state

  112. Store current state action + next state state

  113. Store state View Store Dispatcher state action action state action

  114. Store state View Store Dispatcher state action action state action

  115. A SIMPLE EXAMPLE

  116. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBAction

    func buttonTapped(sender: AnyObject) { } }
  117. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBAction

    func buttonTapped(sender: AnyObject) { if let text = label.text, count = Int(text) { label.text = "\(count + 1)" } } }
  118. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBAction

    func buttonTapped(sender: AnyObject) { } }
  119. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! private

    var count: Int @IBAction func buttonTapped(sender: AnyObject) { count += 1 } }
  120. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! private

    var count: Int @IBAction func buttonTapped(sender: AnyObject) { count += 1 label.text = "\(count)" } }
  121. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBAction

    func buttonTapped(sender: AnyObject) { } }
  122. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBAction

    func buttonTapped(sender: AnyObject) { dispatcher.dispatch(.Increment) } }
  123. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBAction

    func buttonTapped(sender: AnyObject) { dispatcher.dispatch(.Increment) } } extension CounterViewController: Subscriber { }
  124. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBAction

    func buttonTapped(sender: AnyObject) { dispatcher.dispatch(.Increment) } } extension CounterViewController: Subscriber { func receive(state: State) { label.text = "\(state.count)" } }
  125. WHAT DID THIS ACCOMPLISH

  126. Isolates state to a single component WHAT DID THIS ACCOMPLISH

  127. Isolates state to a single component Uses values to communicate

    state WHAT DID THIS ACCOMPLISH
  128. Isolates state to a single component Uses values to communicate

    state Eliminates state from the UI layer WHAT DID THIS ACCOMPLISH
  129. None
  130. Handle view reuse Support complex, dynamic UIs Automatically animate changes

    COLLECTION VIEWS
  131. Buggy Crashy Frustrating COLLECTION VIEWS

  132. Buggy Crashy Frustrating Why? COLLECTION VIEWS

  133. COLLECTION VIEWS

  134. Contrast between implementations COLLECTION VIEWS

  135. Contrast between implementations Trickiness of mutability COLLECTION VIEWS

  136. Contrast between implementations Trickiness of mutability Statelessness and testability COLLECTION

    VIEWS
  137. Contrast between implementations Trickiness of mutability Statelessness and testability Statelessness

    is the future COLLECTION VIEWS
  138. Collection View Data Source

  139. Collection View Data Source how many sections?

  140. 3 Collection View sectionCount Data Source how many sections?

  141. 3 1 Collection View sectionCount itemCounts Data Source how many

    sections? items in section 0?
  142. 3 1 item Collection View sectionCount itemCounts visibleItems Data Source

    how many sections? items in section 0? give me item 0, 0
  143. Collection View Data Source View Controller

  144. Collection View Model View Controller

  145. Collection View Model View Controller item item item item

  146. Collection View Model View Controller item delete item item item

  147. Collection View Model View Controller item delete item delete item

    item
  148. Collection View Model View Controller item delete item delete item

    1 items in section 0? item
  149. Collection View Model View Controller item delete item delete item

    insert item 1 items in section 0? item item
  150. Collection View Model View Controller item delete item delete item

    insert item insert item 2 items in section 0? 1 items in section 0? item item item
  151. PITFALLS

  152. Business logic in the model PITFALLS

  153. Business logic in the model No knowledge of the past

    PITFALLS
  154. Business logic in the model No knowledge of the past

    Mutable state breeds complexity PITFALLS
  155. Collection View Model View Controller

  156. Collection View Model View Controller insert item

  157. Collection View Model View Controller insert item item item

  158. Collection View Model View Controller insert item insert item item

    item item
  159. Collection View Model View Controller insert item insert item 2

    items in section 0? item item item
  160. Collection View Model View Controller insert item insert item 2

    items in section 0? item item item
  161. Collection View Model View Controller insert item insert item 2

    items in section 0? item item insert item item item
  162. Collection View Model View Controller insert item insert item 2

    items in section 0? item item delete item delete item 1 items in section 0? insert item
  163. Collection View Model View Controller insert item insert item 2

    items in section 0? item item delete item delete item 1 items in section 0? insert item
  164. reloadItemsAtIndexPaths reloadSections reloadData EXTREME MEASURES

  165. None
  166. current state

  167. current state next state

  168. current state next state current state next state changeset

  169. current state next state current state next state changeset Collection

    View
  170. current state next state current state next state changeset next

    state current state next state changeset nex curre nex cha Collection View Collection View Collecti
  171. current state next state changeset

  172. current state next state changeset Grapes Nutella Gummi bears Pizza

  173. current state next state changeset Grapes Nutella Gummi bears Pizza

    Grapes Nutella Doritos Gummi bears Pizza →
  174. current state next state changeset Grapes Nutella Gummi bears Pizza

    Grapes Nutella Doritos Gummi bears Pizza Insert at index 2 →
  175. current state next state changeset Grapes Nutella Gummi bears Pizza

    Grapes Nutella Doritos Gummi bears Pizza Insert at index 2 → Collection view states are lists of items Only needs to be written once Huge win for testability
  176. struct Item { let title: String }

  177. struct Item { let title: String } extension Item: Equatable

    {} func ==(lhs: Item, rhs: Item) -> Bool { return lhs.title == rhs.title }
  178. struct Item { let title: String } extension Item: Equatable

    {} func ==(lhs: Item, rhs: Item) -> Bool { return lhs.title == rhs.title } let item1 = Item(title: "Doritos") let item2 = Item(title: "Cheetos") item1 == item2 // false
  179. protocol Identifiable { var id: String { get } }

  180. protocol Identifiable { var id: String { get } }

    struct Item: Equatable, Identifiable { let id: String let title: String }
  181. protocol Identifiable { var id: String { get } }

    struct Item: Equatable, Identifiable { let id: String let title: String } let item1 = Item(id: "1", title: "Doritos") let item2 = Item(id: "1", title: "Cheetos") item1.id == item2.id // true item1 == item2 // false
  182. let state1 = [Item(id: "A", title: "Gummi bears")] let state2

    = [Item(id: "B", title: "Cheetos")] state1.difference(state2) // [.Delete(0), .Insert(0)]
  183. let state1 = [Item(id: "A", title: "Gummi bears")] let state2

    = [Item(id: "B", title: "Cheetos")] state1.difference(state2) // [.Delete(0), .Insert(0)] extension CollectionType where Generator.Element: Equatable, Generator.Element: Identifiable { }
  184. let state1 = [Item(id: "A", title: "Gummi bears")] let state2

    = [Item(id: "B", title: "Cheetos")] state1.difference(state2) // [.Delete(0), .Insert(0)] extension CollectionType where Generator.Element: Equatable, Generator.Element: Identifiable { func difference(other: Self) -> [Change] { var changes = [Change]() // TODO return changes } }
  185. let state1 = [Item(id: "A", title: "Gummi bears")] let state2

    = [Item(id: "B", title: "Cheetos")] state1.difference(state2) // [.Delete(0), .Insert(0)] extension CollectionType where Generator.Element: Equatable, Generator.Element: Identifiable { func difference(other: Self) -> [Change] { var changes = [Change]() // TODO return changes } } enum Change { case Delete(Int) case Insert(Int) case Update(Int) }
  186. for (index, item) in other.enumerate() { if !contains({ $0.id ==

    item.id }) { changes.append(.Insert(index)) } } for (index, item) in enumerate() { if !other.contains({ $0.id == item.id }) { changes.append(.Delete(index)) } } for (index, item) in enumerate() { if other.contains({ $0.id == item.id && $0 != item }) { changes.append(.Update(index)) } }
  187. for (index, item) in other.enumerate() { if !contains({ $0.id ==

    item.id }) { changes.append(.Insert(index)) } } for (index, item) in enumerate() { if !other.contains({ $0.id == item.id }) { changes.append(.Delete(index)) } } for (index, item) in enumerate() { if other.contains({ $0.id == item.id && $0 != item }) { changes.append(.Update(index)) } }
  188. for (index, item) in other.enumerate() { if !contains({ $0.id ==

    item.id }) { changes.append(.Insert(index)) } } for (index, item) in enumerate() { if !other.contains({ $0.id == item.id }) { changes.append(.Delete(index)) } } for (index, item) in enumerate() { if other.contains({ $0.id == item.id && $0 != item }) { changes.append(.Update(index)) } }
  189. for (index, item) in other.enumerate() { if !contains({ $0.id ==

    item.id }) { changes.append(.Insert(index)) } } for (index, item) in enumerate() { if !other.contains({ $0.id == item.id }) { changes.append(.Delete(index)) } } for (index, item) in enumerate() { if other.contains({ $0.id == item.id && $0 != item }) { changes.append(.Update(index)) } }
  190. Goes against the grain of UIKit Animations Performance Debugging CHALLENGES

  191. Encapsulate complete app state Test fuzzing Time travel CURRENT WORK

  192. THANKS!

  193. http://livefront.com/speaks/stateless/ Adam May @yammada Sam Kirchmeier @skirchmeier THANKS!