{"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":{"slug":"/post/2023/03/adapting-reference-semantics-model-in-swiftui-the-basics-f521","title":"在 SwiftUI 中适配引用语义模型 -- 基础篇","subtitle":"","createdTime":"2023-03-02T00:00:00.000Z","tags":["SwiftUI","Swift","适配器模式","引用语义","Binding"],"category":"Programming","file":{"childMdx":{"excerpt":"介绍 最近，我的一位同事试图将引用语义模型用  ObservableObject  和  @StateObject  迁移到 SwiftUI。由于网上有很多关于用这种方式来迁移引用语义模型到 SwiftUI 的例子，在这篇文章中我不打算重点讨论这种方法的一般情况，而是想重点讨论在我和我的同事寻找答案使其代码正常工作的过程中发现的更有价值的三个话题。这些话题是： 在 SwiftUI 中真相源 (Source of Truth) 是什么？ 在 SwiftUI…"}}}},"pageContext":{"postId":"5c75555f-38a8-50fe-b14c-454701f208be","earlierPostId":"5b9ea0ff-9d12-540c-a169-462414cddcb7","laterPostId":"50422053-00c8-522e-a118-2c12b7f4d13f"}}