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

Declarative UI without SwiftUI

Declarative UI without SwiftUI

DeclaratvieUI is a good pattern for crafting complex UI components while keeping the states simple. SwiftUI is one of the best frameworks of Declarative UI pattern. However, due to the ABI stability, we cannot use SwiftUI, or even those syntax supporting declarative programming, on devices below iOS 13.
In this slide, I briefly introduce the main idea of the declarative pattern, and demonstrate how to implement the pattern based on older SDKs.

(投影片內容為中文 Chinese Content)

Avatar for Shih Ting Huang (Neo)

Shih Ting Huang (Neo)

September 21, 2019
Tweet

Other Decks in Technology

Transcript

  1. About me • 從 Objective-C 開始入坑的 iOS ⼯工程師 • 主修電機⼯工程、⾃自然語⾔言處理理

    • 業餘前端 (react.js) • AppCoda TW 作者 • Tokyo WebHack Meetup 組織者 • Blog: 鍵盤藍藍綠藻、S.T.H Brewery (拖稿中) 黃⼠士庭 Neo
  2. ⼤大綱 • 背景 • 甚麼是 Declarative Programming • 怎樣有效率地更更新 UI

    • 怎樣讓語法更更 declarative • 在實務上怎麼實作?
  3. UI = 資料 + 狀狀態 title title title UI Data

    { "id": 1, "title": "A title" }, { "id": 2, "title": "A title" }, { "id": 3, "title": "A title" }
  4. UI = 資料 + 狀狀態 title loading download title title

    UI Data { "id": 1, "title": "A title" }, { "id": 2, "title": "A title" }, { "id": 3, "title": "A title" } State struct Item { var isLoading: Bool var isDownloaded: Bool }
  5. UI = 資料 + 狀狀態 title loading download title title

    func downloadBtnPressed(at indexPath: IndexPath) { if let cell = tableView.cellForRow( at: indexPath) as? ListTableViewCell { cell.downloadBtn.state = .loading stateArray[indexPath.row] = .loading } }
  6. UI = 資料 + 狀狀態 title loading download title title

    func downloadBtnPressed(at indexPath: IndexPath) { if let cell = tableView.cellForRow( at: indexPath) as? ListTableViewCell { cell.downloadBtn.state = .loading stateArray[indexPath.row] = .loading } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { cell.downloadBtn.state = stateArray[indexPath.row] }
  7. UI = 資料 + 狀狀態 title + download title title

    func downloadBtnPressed(at indexPath: IndexPath) { if let cell = tableView.cellForRow( at: indexPath) as? ListTableViewCell { cell.downloadBtn.state = .loading stateArray[indexPath.row] = .loading } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { cell.downloadBtn.state = stateArray[indexPath.row] }
  8. UI = 資料 + 狀狀態 title + download title title

    func downloadBtnPressed(at indexPath: IndexPath) { if let cell = tableView.cellForRow( at: indexPath) as? ListTableViewCell { cell.downloadBtn.state = .loading stateArray[indexPath.row] = .loading } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { cell.downloadBtn.state = stateArray[indexPath.row] } func addBookmark(indexPath: IndexPath) { bookmarkArray[indexPath.row] = .bookmarked }
  9. UI = 資料 + 狀狀態 title + download title title

    func downloadBtnPressed(at indexPath: IndexPath) { if let cell = tableView.cellForRow( at: indexPath) as? ListTableViewCell { cell.downloadBtn.state = .loading stateArray[indexPath.row] = .loading cell.bookmarkBtn.state = .bookmarked } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { cell.downloadBtn.state = stateArray[indexPath.row] } func addBookmark(indexPath: IndexPath) { bookmarkArray[indexPath.row] = .bookmarked }
  10. UI = 資料 + 狀狀態 title + download title title

    loading func downloadBtnPressed(at indexPath: IndexPath) { if let cell = tableView.cellForRow( at: indexPath) as? ListTableViewCell { cell.downloadBtn.state = .loading stateArray[indexPath.row] = .loading cell.bookmarkBtn.state = .bookmarked } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { cell.downloadBtn.state = stateArray[indexPath.row] } func addBookmark(indexPath: IndexPath) { bookmarkArray[indexPath.row] = .bookmarked }
  11. SwiftUI title loading download title title Cell var body: some

    View { HStack { Text(item.title) Spacer() Button(item.loadingText) } }
  12. SwiftUI title loading download title title var body: some View

    { HStack { Text(item.title) Spacer() Button(item.loadingText) } } Cell
  13. SwiftUI title loading download title title }var body: some View

    { List(items) { item in Cell(item) } } List
  14. 直觀作法 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

    { ... cell.setup(items[indexPath.row]) ... } Datasource var items: [CellItem] { didSet { tableView.reloadData() } } 資料
  15. 直觀作法 var items: [CellItem] { didSet { tableView.reloadData() } }

    更更新資料 items[3].titleText = "New Title!"
  16. Edit distance 演算法 s w i f t s h

    i f t y • remove w at 1 • insert h at 1 • insert y at 5
  17. 怎麼找出差異異? A A A A A B [ ] [

    ] • remove A at 1 • insert B at 1
  18. 找出不同 diffs = [.remove(1), .insert(B, at: 1)] for diff in

    diffs { switch diff { case .insert(let index): tableView.insertRows(xxx) case .remove(let index): tableView.deleteRows(xxx) } }
  19. Edit distance 演算法 • Wagner–Fischer algorithm: O(MN) • Heckel algorithm:

    Linear time • Myers algorithm: O(ND) Diffing.swift on Swift 5.1
  20. Edit distance 演算法 • https://github.com/ra1028/DifferenceKit with Heckel • https://github.com/onmyway133/DeepDiff with

    Heckel • https://github.com/jflinter/Dwifft with Myers • https://github.com/tonyarnold/Differ with Myers
  21. 資料 • 簡單的 struct 代表 UI 的狀狀態 • 必需是 Hashable

    • 多數狀狀況可以被 compiler auto-synthesis • ⼩小⼼心NSObject!! struct CellItem: Hashable { var title: String var backgroundColor: UIColor }
  22. 資料 • 簡單的 struct 代表 UI 的狀狀態 • 必需是 Hashable

    • 多數狀狀況可以被 compiler auto-synthesis • ⼩小⼼心NSObject!! struct CellItem: Hashable { var title: String var backgroundColor: UIColor }
  23. Edit distance calculator var items: [CellItems] { didSet { let

    diffs = items.difference(from: oldValue) tableView.apply(diffs) } }
  24. Edit distance calculator var items: [CellItems] { didSet { let

    diffs = items.difference(from: oldValue) tableView.apply(diffs) } }
  25. Layout Renderer extension UITableView { func apply(diffs: CollectionDifference) { self.beginUpdates()

    for diff in diffs { switch diff { case .insert(let index): self.insertRows(at: index) case .remove(let index): self.deleteRows(at: index) } } self.endUpdates() } }
  26. Layout Renderer extension UITableView { func apply(diffs: CollectionDifference) { self.beginUpdates()

    for diff in diffs { switch diff { case .insert(let index): self.insertRows(at: index) case .remove(let index): self.deleteRows(at: index) } } self.endUpdates() } }
  27. SwiftUI title loading download title title Cell var body: some

    View { HStack { Text(item.title) Spacer() Button(item.loadingText) } }
  28. Declarative style text3 text2 text1 func render() { hostView.update([ Label(text1),

    Label(text2), Label(text3), ]) } struct Label: Hashable { var text: String init(_ text: String) { self.text = text } }
  29. Sub-class: UIStackView private var current: [Label] = [] func update(_

    updated: [Label]) { let diffs = updated.difference(from: current) for diff in diffs { switch diff { case .insert(let index): insertArrangedSubview(labelView, at: offset) case .remove(let index): arrangedSubviews[index].removeFromSuperview() } } current = updated }
  30. Sub-class: UIStackView private var current: [Label] = [] func update(_

    updated: [Label]) { let diffs = updated.difference(from: current) for diff in diffs { switch diff { case .insert(let index): insertArrangedSubview(labelView, at: offset) case .remove(let index): arrangedSubviews[index].removeFromSuperview() } } current = updated }
  31. 更更新資料 func render() { hostView.update([ Label(text1), Label(text2), Label(text3), ]) }

    text1 = "New value" var text1: String { didSet { render() } }
  32. 更更新資料 func render() { hostView.update([ Label(text1), Label(text2), Label(text3), ]) }

    text1 = "New value" var text1: String { didSet { render() } }
  33. 重覆的部份很多 var text1: String { didSet { render() } }

    var text2: String { didSet { render() } } var text3: String { didSet { render() } }
  34. 簡單的Wrapper class StateStore<T: Hashable>: Hashable { private weak var renderer:

    Renderer? var value: T { didSet { assert(renderer != nil) renderer?.render() } } init(renderer: Renderer, value: T) { self.renderer = renderer self.value = value } }
  35. 簡單的Wrapper let text1 = StateStore(renderer: self, value:"Label 1”) let text2

    = StateStore(renderer: self, value:"Label 1”) let text3 = StateStore(renderer: self, value:"Label 1”) func render() { hostView.update([ Label(text1), Label(text2), Label(text3), ]) } text1.value = "update!"
  36. 參參考資料 • Swift 5.1 Collection Diffing - Federico Zanetello •

    A better way to update UICollectionView data in Swift with diff framework - Khoa Pham • Introduction to declarative UI - Flutter • DifferenceKit - Ryo Aoyama