Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Sam Kirchmeier

April 23, 2016
Tweet

More Decks by Sam Kirchmeier

Other Decks in Programming

Transcript

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

    examples Discuss challenges and future work TOPICS
  2. 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
  3. Mutable via private methods Indirectly mutable via public methods ENCAPSULATION

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

    number of moving parts ENCAPSULATION var str = "Hello, playground" class NeatlyEncapsulated { private }
  5. Does not depend on variables Same inputs same outputs No

    side effects WHAT DOES IT MEAN TO BE STATELESS?
  6. Code that relies on constants is stateless CONSTANTS let str

    = "Hello, playground" str = "Goodbye!" // Compiler error!
  7. Int, Float, Double String Array, Dictionary, Set Struct, Enum Code

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

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

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

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

    credenza = Product(price: "$11.00") var niceCredenza = credenza niceCredenza.price = "$12.00" credenza.price // "$11.00"
  12. Eliminates and isolates state Takes advantage of Swift’s constants Takes

    advantage of Swift’s value types WE NEED A NEW PARADIGM
  13. View action Represents an interaction Is not state Includes an

    action type Includes relevant data Is a value
  14. 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)" } } }
  15. class CounterViewController: UIViewController { @IBOutlet weak var label: UILabel! private

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

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

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

    func buttonTapped(sender: AnyObject) { dispatcher.dispatch(.Increment) } } extension CounterViewController: Subscriber { }
  19. 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)" } }
  20. Isolates state to a single component Uses values to communicate

    state Eliminates state from the UI layer WHAT DID THIS ACCOMPLISH
  21. 3 1 item Collection View sectionCount itemCounts visibleItems Data Source

    how many sections? items in section 0? give me item 0, 0
  22. Collection View Model View Controller item delete item delete item

    insert item 1 items in section 0? item item
  23. 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
  24. Business logic in the model No knowledge of the past

    Mutable state breeds complexity PITFALLS
  25. Collection View Model View Controller insert item insert item 2

    items in section 0? item item insert item item item
  26. 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
  27. 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
  28. 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
  29. current state next state changeset Grapes Nutella Gummi bears Pizza

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

    Grapes Nutella Doritos Gummi bears Pizza Insert at index 2 →
  31. 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
  32. struct Item { let title: String } extension Item: Equatable

    {} func ==(lhs: Item, rhs: Item) -> Bool { return lhs.title == rhs.title }
  33. 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
  34. protocol Identifiable { var id: String { get } }

    struct Item: Equatable, Identifiable { let id: String let title: String }
  35. 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
  36. let state1 = [Item(id: "A", title: "Gummi bears")] let state2

    = [Item(id: "B", title: "Cheetos")] state1.difference(state2) // [.Delete(0), .Insert(0)]
  37. 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 { }
  38. 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 } }
  39. 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) }
  40. 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)) } }
  41. 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)) } }
  42. 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)) } }
  43. 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)) } }