Slide 1

Slide 1 text

Declarative UI on iOS Neo

Slide 2

Slide 2 text

About me • 從 Objective-C 開始入坑的 iOS ⼯工程師 • 主修電機⼯工程、⾃自然語⾔言處理理 • 業餘前端 (react.js) • AppCoda TW 作者 • Tokyo WebHack Meetup 組織者 • Blog: 鍵盤藍藍綠藻、S.T.H Brewery (拖稿中) 黃⼠士庭 Neo

Slide 3

Slide 3 text

About me 台北 東京

Slide 4

Slide 4 text

About me

Slide 5

Slide 5 text

About me • • • • ☕

Slide 6

Slide 6 text

⼤大綱 • 背景 • 甚麼是 Declarative Programming • 怎樣有效率地更更新 UI • 怎樣讓語法更更 declarative • 在實務上怎麼實作?

Slide 7

Slide 7 text

UI 的挑戰

Slide 8

Slide 8 text

過去的 UI

Slide 9

Slide 9 text

現代的 UI

Slide 10

Slide 10 text

UI = 資料 + 狀狀態 title title title UI Data { "id": 1, "title": "A title" }, { "id": 2, "title": "A title" }, { "id": 3, "title": "A title" }

Slide 11

Slide 11 text

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 }

Slide 12

Slide 12 text

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 } }

Slide 13

Slide 13 text

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] }

Slide 14

Slide 14 text

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] }

Slide 15

Slide 15 text

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 }

Slide 16

Slide 16 text

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 }

Slide 17

Slide 17 text

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 }

Slide 18

Slide 18 text

直接操作 UI 元件 cell.downloadBtn.state = .loading cell.titleLabel.text = "title" cell.backgroundColor = .gray

Slide 19

Slide 19 text

直接操作 UI 元件 title loading download title title program Imperative Programming

Slide 20

Slide 20 text

有沒有更更直觀的⽅方式?

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

SwiftUI title loading download title title }var body: some View { List(items) { item in Cell(item) } } List

Slide 24

Slide 24 text

直接操作 UI 元件 cell.titleLabel.text = item.title cell.downloadBtn.state = item.loadingState

Slide 25

Slide 25 text

讓 UI ⾃自⼰己對資料做出反應 HStack { Text(item.title) Spacer() Button(item.loadingState) }

Slide 26

Slide 26 text

Declarative UI title loading download title title item item item Declarative Programming

Slide 27

Slide 27 text

Declarative UI • 只要告訴 UI 最終的狀狀態 • UI ⾃自⼰己會判別狀狀態的變化做更更新

Slide 28

Slide 28 text

But!

Slide 29

Slide 29 text

SwiftUI & iOS 13

Slide 30

Slide 30 text

SwiftUI & iOS 13

Slide 31

Slide 31 text

How?

Slide 32

Slide 32 text

How?

Slide 33

Slide 33 text

來來分解 Declarative UI

Slide 34

Slide 34 text

來來分解 Declarative UI title loading download title title title title title UI Data

Slide 35

Slide 35 text

來來分解 Declarative UI title loading download title title title title title UI Data

Slide 36

Slide 36 text

直觀作法 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { ... cell.setup(items[indexPath.row]) ... } Datasource

Slide 37

Slide 37 text

直觀作法 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { ... cell.setup(items[indexPath.row]) ... } Datasource var items: [CellItem] { didSet { tableView.reloadData() } } 資料

Slide 38

Slide 38 text

直觀作法 var items: [CellItem] { didSet { tableView.reloadData() } } 更更新資料 items[3].titleText = "New Title!"

Slide 39

Slide 39 text

直觀作法 title loading download title2 title title title2 title UI State reloadData( )

Slide 40

Slide 40 text

直觀作法

Slide 41

Slide 41 text

很⽅方便便,但是...

Slide 42

Slide 42 text

⼤大量量重覆的運算 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { ... cell.setup(items[indexPath.row]) ... }

Slide 43

Slide 43 text

只更更新狀狀態有變化的部分

Slide 44

Slide 44 text

找出不同 title loading download title2 title title title2 title UI Data reload

Slide 45

Slide 45 text

找出不同 title title title Old title title2 title New

Slide 46

Slide 46 text

找出不同 title title Old title title New remove title2

Slide 47

Slide 47 text

找出不同 title title title Old title title New insert title2 title2

Slide 48

Slide 48 text

找出不同 tableView.reloadData() tableView.deleteRows(at: [IndexPath(row: 1, section: 0)], with: .automatic) tableView.insertRows(at: [IndexPath(row: 1, section: 0)], with: .automatic)

Slide 49

Slide 49 text

怎麼找出差異異?

Slide 50

Slide 50 text

怎麼找出差異異? title title title Old title title2 title New

Slide 51

Slide 51 text

怎麼找出差異異? title title title title title2 title protocol Hashable { ... } = = != A A A A A B

Slide 52

Slide 52 text

怎麼找出差異異? = = != A A A A A B [ ] [ ]

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

怎麼找出差異異? A A A A A B [ ] [ ]

Slide 55

Slide 55 text

怎麼找出差異異? A A A A A B [ ] [ ] • remove A at 1 • insert B at 1

Slide 56

Slide 56 text

找出不同 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) } }

Slide 57

Slide 57 text

Edit distance 演算法 • Wagner–Fischer algorithm: O(MN) • Heckel algorithm: Linear time • Myers algorithm: O(ND)

Slide 58

Slide 58 text

Edit distance 演算法 • Wagner–Fischer algorithm: O(MN) • Heckel algorithm: Linear time • Myers algorithm: O(ND) Diffing.swift on Swift 5.1

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

來來看看程式上的架構

Slide 61

Slide 61 text

架構 data Edit distance Calculator Layout Renderer old data

Slide 62

Slide 62 text

架構 data Edit distance Calculator Layout Renderer old data

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

資料 struct CellItem: Hashable { var title: String var backgroundColor: UIColor } var items: [CellItems]

Slide 66

Slide 66 text

架構 state Edit distance Calculator Layout Renderer old state

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

架構 state Edit distance Calculator Layout Renderer old state

Slide 70

Slide 70 text

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() } }

Slide 71

Slide 71 text

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() } }

Slide 72

Slide 72 text

找出不同 title loading download title2 title title title2 title UI Data reload

Slide 73

Slide 73 text

不夠 declarative

Slide 74

Slide 74 text

架構 state Edit distance Calculator Layout Renderer old state

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

當然不⾏行行

Slide 77

Slide 77 text

長得像就好

Slide 78

Slide 78 text

⽬目標 text3 text2 text1

Slide 79

Slide 79 text

Declarative style func render() { hostView.update([ Label(text1), Label(text2), Label(text3), ]) } text1 = "New value" text3 text2 text1

Slide 80

Slide 80 text

架構 text3 text2 text1 hostView: UIStackView 新的狀狀態 舊的狀狀態 difference insertArrangedSubview removeFromSuperview

Slide 81

Slide 81 text

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 } }

Slide 82

Slide 82 text

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 }

Slide 83

Slide 83 text

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 }

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

重覆的部份很多 var text1: String { didSet { render() } } var text2: String { didSet { render() } } var text3: String { didSet { render() } }

Slide 88

Slide 88 text

簡單的Wrapper class StateStore: 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 } }

Slide 89

Slide 89 text

簡單的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!"

Slide 90

Slide 90 text

更更複雜的狀狀況 View state

Slide 91

Slide 91 text

更更複雜的狀狀況 render( )

Slide 92

Slide 92 text

最後

Slide 93

Slide 93 text

Demo

Slide 94

Slide 94 text

Demo https://github.com/koromiko/SimpleDeclarativeSyntax

Slide 95

Slide 95 text

Demo

Slide 96

Slide 96 text

Declarative UI Vue.js

Slide 97

Slide 97 text

參參考資料 • 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

Slide 98

Slide 98 text

來來聊天吧 • Twitter: @KoromikoNeo • GitHub: https://github.com/koromiko • Website: https://huangshihting.works/ • LinkedIn: https://www.linkedin.com/in/huangshihting/

Slide 99

Slide 99 text

Thank you