{"data":{"post":{"title":"在 SwiftUI 中使用函數式 Binding 實現觀察者模式","subtitle":"","isPublished":true,"createdTime":"2022-08-19T00:00:00.000Z","lastModifiedTime":null,"license":null,"tags":["SwiftUI","Binding","Swift","觀察者模式"],"category":"編程","file":{"childMdx":{"excerpt":"故事 這週，我的同事問了我一個問題：在 SwiftUI 中怎麼觀察用戶對  Picker…","code":{"body":"function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\nconst layoutProps = {};\nreturn class MDXContent extends React.Component {\n  constructor(props) {\n    super(props);\n    this.layout = null;\n  }\n\n  render() {\n    const _this$props = this.props,\n          {\n      components\n    } = _this$props,\n          props = _objectWithoutProperties(_this$props, [\"components\"]);\n\n    return React.createElement(MDXTag, {\n      name: \"wrapper\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"h2\",\n      components: components\n    }, `故事`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `這週，我的同事問了我一個問題：在 SwiftUI 中怎麼觀察用戶對 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Picker`), ` 的選擇行爲？`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `這是一個來自真實業務的問題，所以我覺得值得我花費時間去解決它。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `範例代碼如下所示，然後我的同事想觀察用戶對 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Picker`), ` 候選項的選擇行爲。`), React.createElement(MDXTag, {\n      name: \"pre\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"code\",\n      components: components,\n      parentName: \"pre\",\n      props: {\n        \"className\": \"language-swift\"\n      }\n    }, `import SwiftUI\n\nlet labels = [\"One\", \"Two\", \"Three\", \"Four\"]\n\nstruct ContentView: View {\n  \n  var data = Array(labels.enumerated())\n  \n  @State\n  var selection: Int = 0\n  \n  var body: some View {\n    Picker(\"Picker\", selection: $selection) {\n      ForEach(data, id: \\\\.offset) { (_, label) in\n        Text(label)\n      }\n    }.pickerStyle(.inline)\n  }\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"h2\",\n      components: components\n    }, `分析`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `但是，「觀察」本身的意義可能會隨着上下文變動而變動：`), React.createElement(MDXTag, {\n      name: \"ul\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"li\",\n      components: components,\n      parentName: \"ul\"\n    }, `它可以表示用戶在 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"li\"\n    }, `Picker`), ` 上放下手指的那一刻。`), React.createElement(MDXTag, {\n      name: \"li\",\n      components: components,\n      parentName: \"ul\"\n    }, `它可以表示用戶在 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"li\"\n    }, `Picker`), ` 上擡起手指的那一刻。`), React.createElement(MDXTag, {\n      name: \"li\",\n      components: components,\n      parentName: \"ul\"\n    }, `它可以表示 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"li\"\n    }, `Picker`), ` 對 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"li\"\n    }, `$selection`), ` 進行值變更的那一刻。`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `上述每一項都將導致不同的最終解決方案。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `因爲 SwiftUI 控件可以使用 style 修飾器，而 style 修飾器可以改變控件的樣式和行爲，想達成上述對用戶行爲的觀察中的前兩種需要對控件本身做深度定製。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `但是如果你只是想觀察 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Picker`), ` 對 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `$selection`), ` 進行值變更的那個時刻，你一定要試試函數式 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), `。`), React.createElement(MDXTag, {\n      name: \"blockquote\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"p\",\n      components: components,\n      parentName: \"blockquote\"\n    }, `等等！已經有 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `onChange(of:, perform:)`), ` 修飾器了，爲什麼我要用你說的函數式 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), `？`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `好的。我們已經觸及了我同事問題的關鍵：`, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `onChange(of:, perform:)`), ` 的時機很難預測和控制。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `在我同事的代碼中，他使用 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `onChange(of: perform)`), ` 觸發了網絡請求和用戶行爲觀察。但是網絡請求的回調總是比用戶行爲觀察回調要早 30ms。這個現象是因爲 SwiftUI 的求值順序造成的。你可以通過將 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `onChange(of:, perform:)`), ` 精心排列在視圖樹上來控制這個順序。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `但是我們在做工程——我們不能將修飾器的放置位置與 SwiftUI 的求值順序耦合起來！`), React.createElement(MDXTag, {\n      name: \"h2\",\n      components: components\n    }, `解決方案`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `爲了解決這個問題，我建議我的同事將 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `$selection`), ` 通過下面这个 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 的 initializer 包裝起來。`), React.createElement(MDXTag, {\n      name: \"pre\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"code\",\n      components: components,\n      parentName: \"pre\",\n      props: {\n        \"className\": \"language-swift\"\n      }\n    }, `public struct Binding<Value> {\n\n  public init(\n    get: @escaping () -> Value,\n    set: @escaping (Value, Transaction) -> Void\n  )\n\n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `這就是我所說的函數式 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), `。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `以下是結合文章開頭範例代碼之後的這個 initializer 的用例：`), React.createElement(MDXTag, {\n      name: \"pre\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"code\",\n      components: components,\n      parentName: \"pre\",\n      props: {\n        \"className\": \"language-swift\"\n      }\n    }, `struct ContentView: View {\n  \n  // ...\n\n  var selectionBinding: Binding<Int> {\n    Binding(\n      get: {\n        $selection.wrappedValue\n      },\n      set: { (newValue, tnx) in\n        // 調用 \\`.transaction\\` 修飾器以利用 \\`tnx : Transaction\\` 對象.\n        $selection.transaction(tnx).wrappedValue = newValue\n        observeSelectionChange()\n      }\n    )\n  }\n\n  func observeSelectionChange() {\n    // 做你想做的\n  }\n \n  var body: some View {\n    Picker(\"Picker\", selection: selectionBinding) {\n      ForEach(data, id: \\\\.offset) { (_, label) in\n        Text(label)\n      }\n    }.pickerStyle(.inline)\n  }\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `在 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `observeSelectionChange`), ` 函數中你就可以組織你自己的用戶行爲觀察邏輯了。`), React.createElement(MDXTag, {\n      name: \"h2\",\n      components: components\n    }, `結論`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `這裏有幾個原因我建議你使用這種方式觀察用戶行爲：`), React.createElement(MDXTag, {\n      name: \"ul\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"li\",\n      components: components,\n      parentName: \"ul\"\n    }, React.createElement(MDXTag, {\n      name: \"p\",\n      components: components,\n      parentName: \"li\"\n    }, `SwiftUI 是值變更驅動的。這意味着值變更在一個運行中的 SwiftUI 程序中無處不在，並且爲我們提供了很多觀察點。`)), React.createElement(MDXTag, {\n      name: \"li\",\n      components: components,\n      parentName: \"ul\"\n    }, React.createElement(MDXTag, {\n      name: \"p\",\n      components: components,\n      parentName: \"li\"\n    }, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 比你想象的更強大。它支持針對 key-path 和 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Collection`), ` 下標的 projection。這讓開發者可以將局部的值變更傳導到一個控件或者 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `View`), ` 上。這也意味着你可以通過嵌套函數式 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 來觀察蘋果一方控件以及良好設計的三方控件上的上述各種各樣的 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 傳導的變更。`)), React.createElement(MDXTag, {\n      name: \"li\",\n      components: components,\n      parentName: \"ul\"\n    }, React.createElement(MDXTag, {\n      name: \"p\",\n      components: components,\n      parentName: \"li\"\n    }, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 可以觀察所有驅動 SwiftUI 進行視圖更新的內容變更。相比之下，`, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `onChange(of:, perform:)`), ` 要求開發者觀察 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Equatable`), ` 的值。但是有許多不遵從 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Equatable`), ` 的類型也可以驅動 SwiftUI 進行視圖更新。這裏有一個例子：閉包。`)), React.createElement(MDXTag, {\n      name: \"li\",\n      components: components,\n      parentName: \"ul\"\n    }, React.createElement(MDXTag, {\n      name: \"p\",\n      components: components,\n      parentName: \"li\"\n    }, `在一個函數式 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 中你可以選擇在值變更之前或者之後觀察，你也可以控制值變更帶來的多個行爲的順序。`))));\n  }\n\n}\nMDXContent.isMDXComponent = true;","scope":""},"headings":[{"value":"故事","depth":2},{"value":"分析","depth":2},{"value":"解決方案","depth":2},{"value":"結論","depth":2}]}}},"earlierPostExcerpt":{"slug":"/post/2022/03/unexplained-swiftui-the-programming-language-nature-of-swiftui-d20e","title":"SwiftUI 探祕 - SwiftUI 的編程語言本質","subtitle":"","createdTime":"2022-03-06T00:00:00.000Z","tags":["SwiftUI 探祕","SwiftUI","Swift"],"category":"編程","file":{"childMdx":{"excerpt":"前言 蘋果在 WWDC 2019 向開發者介紹了 SwiftUI。多數人也許會將 SwiftUI 看成又一個如  Flutter  或者  React.js  又或者  Vue.js  這樣踩在聲明式、無狀態 UI 編程潮流浪尖的 UI 框架。雖然 SwiftUI 與上述框架有着非常多的共同點，但是 SwiftUI 從設計到實現上都與上述框架有着本質的不同。 實際上，相較於是一個編程框架，SwiftUI 更加像是一種編程語言。不相信？讓我來看看一個用「原生」SwiftUI 代碼編寫的斐波那契数列計算程序。 然後通過添加以下兩行代碼，我們可以在 Swift Playground…"}}},"laterPostExcerpt":null},"pageContext":{"postId":"1c2f780d-e035-5c3e-ba96-254e56803c75","earlierPostId":"1488d9d1-b04a-52ec-bff4-f6a0e73301f8","laterPostId":null}}