{"data":{"post":{"title":"在 SwiftUI 中适配引用语义模型 -- 基础篇","subtitle":"","isPublished":true,"createdTime":"2023-03-02T00:00:00.000Z","lastModifiedTime":null,"license":null,"tags":["SwiftUI","Swift","适配器模式","引用语义","Binding"],"category":"Programming","file":{"childMdx":{"excerpt":"介绍 最近，我的一位同事试图将引用语义模型用  ObservableObject  和  @StateObject  迁移到 SwiftUI…","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    }, `最近，我的一位同事试图将引用语义模型用 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `ObservableObject`), ` 和 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `@StateObject`), ` 迁移到 SwiftUI。由于网上有很多关于用这种方式来迁移引用语义模型到 SwiftUI 的例子，在这篇文章中我不打算重点讨论这种方法的一般情况，而是想重点讨论在我和我的同事寻找答案使其代码正常工作的过程中发现的更有价值的三个话题。这些话题是：`), 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 中真相源 (Source of Truth) 是什么？`)), 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 中如何正确地构成一个真相源？`)), 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: \"p\",\n      components: components\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: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"p\",\n      props: {\n        \"src\": \"/figure-1-expected-behavior-b48113f518a82b5a37b3552e66d526ee.gif\",\n        \"alt\": \"预期行为\",\n        \"title\": \"预期行为\"\n      }\n    })), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `开关下面的 Model 是来自 Objective-C 的遗留代码——当然是引用语义的。此外，setter 是一个异步方法，有一个 completion block。`), 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    }, `class LegacyModel {\n  \n  // getter\n  var isOn: Bool { get }\n  \n  // 异步 setter\n  func setOn(_ newValue: Bool, completion: ((_ success: Bool) -> Void)?)\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    }, `ObservableObject`), ` 来迁移 Objecitve-C 模型，并使用下列异步 setter 进行适配，其中包含：`), 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    }, `一个函数式 `, 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    }, `一个来自 completion block 的对 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `ObservableObject.objectWillChange.send()`), ` 的调用`))), 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\nimport Combine\n\nclass Model: ObservableObject {\n\n  struct Item {\n  \n    let description: String\n    \n    let isOn: Binding<Bool>\n  \n  }\n\n  @Published\n  var items:  [Item] = []\n  \n  init() {\n    items = [\n      Item(\n        description: \"Test\",\n        isOn: Binding {\n          LegecyModel.shared.isOn\n        } set: { newValue, tnx in\n          // 异步设值\n          LegecyModel.shared.setOn(newValue) {\n            DispatchQueue.main.async {\n              self.objectWillChange.send()\n            }\n          }\n        }\n      )\n    ]\n  }\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `下面是实现用户界面的代码。`), 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  @StateObject\n  var model = Model()\n  \n  var body: some View {\n    ItemView(item: model.items[0])\n  }\n  \n  struct ItemView: View {\n  \n    var item: Model.Item\n    \n    @State\n    var needsUserConfirm: Bool = false\n    \n    var body: some View {\n      Button(item.description) {\n        if item.isOn.wrappedValue {\n          item.isOn.wrappedValue = false\n        } else {\n          needsUserConfirm = true\n        }\n      }\n      .buttonStyle(ToggleButtonStyle(isOn: item.isOn.wrappedValue))\n      .alert(\"Sure?\", isPresented: $needsUserConfirm) {\n        Button(\"OK\") {\n          item.isOn.wrappedValue = true\n        }\n        Button(\"Cancel\", role: .cancel) {}\n      }\n    }\n  }\n}\n\n// 我们想要 Toggle 的视觉样式，但是点击行为是 Button 的。\nstruct ToggleButtonStyle: ButtonStyle {\n  \n  var isOn: Bool\n  \n  func makeBody(configuration: Configuration) -> some View {\n    Toggle(isOn: .constant(isOn)) {\n      configuration.label\n    }\n    .allowsHitTesting(false)\n    .contentShape(Rectangle())\n  }\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `但在上面的代码中，他永远无法通过点击「OK」按钮将开关切换为开启状态。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"p\",\n      props: {\n        \"src\": \"/figure-1-expected-behavior-b48113f518a82b5a37b3552e66d526ee.gif\",\n        \"alt\": \"错误的行为\",\n        \"title\": \"错误的行为\"\n      }\n    })), React.createElement(MDXTag, {\n      name: \"h2\",\n      components: components\n    }, `修改一：什么在驱动 SwiftUI 的更新？`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `在第一眼看到这个问题时，我认为他的代码没有遵循 SwiftUI 基于值 (value-based) 的更新模式。\nSwiftUI 会随着值的更新而更新。由于 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item`), ` 中没有存储属性的改变（都是 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `let`), ` 属性），因此 SwiftUI 无法检测 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 背后的变化。`), 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    }, `class Model: ObservableObject {\n\n  struct Item {\n  \n    let description: String\n    \n    let isOn: Binding<Bool>\n  \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    }, `self.objectWillChange.send()`), ` 之后，`, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `ItemView.body`), ` 将不会被求值。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"p\",\n      props: {\n        \"src\": \"/figure-3-breakpoint-for-hack-1-061ae835c949c3939a368da71452d61c.gif\",\n        \"alt\": \"修改一之前断点行为\",\n        \"title\": \"修改一之前断点行为\"\n      }\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    }, `Model`), ` 像下面这样扩充一下：`), 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    }, `class Model: ObservableObject {\n\n  struct Item {\n  \n    let description: String\n    \n    let isOn: Binding<Bool>\n    \n    // 添加字段\n    var seed: UInt8\n  \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    }, `self.objectWillChange.send()`), ` 时通过增加 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item.seed`), ` 的值来驱动 SwiftUI 对 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `ItemView.body`), ` 进行求值了：`), 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    }, `class Model: ObservableObject {\n\n  ...\n  \n  init() {\n    items = [\n      Item(\n        description: \"Test\",\n        isOn: Binding {\n          LegecyModel.shared.isOn\n        } set: { newValue, tnx in\n          // 异步设值\n          LegecyModel.shared.setOn(newValue) {\n            DispatchQueue.main.async {\n              self.objectWillChange.send()\n              self.items[0].seed &+= 1\n            }\n          }\n        }\n      )\n    ]\n  }\n}\n\nstruct ContentView: View {\n\n  ...\n  \n  struct ItemView: View {\n    \n    ...\n    \n    var body: some View {\n      Button(item.description) {\n        ...\n      }\n      .buttonStyle(BySeedUpdateToggleButtonStyle(isOn: item.isOn.wrappedValue, seed: item.seed))\n      ...\n    }\n    \n  }\n  \n}\n\nstruct BySeedUpdateToggleButtonStyle: ButtonStyle {\n  \n  var isOn: Bool\n  \n  var seed: Int\n  \n  func makeBody(configuration: Configuration) -> some View {\n    Toggle(isOn: .constant(isOn)) {\n      configuration.label\n    }\n    .allowsHitTesting(false)\n    .contentShape(Rectangle())\n  }\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `在他修改了代码、编译并运行了程序后，我们期待着奇迹发生——但最终没有任何改变。他仍然无法通过点击「OK」按钮来让打开开关。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"p\",\n      props: {\n        \"src\": \"/figure-2-incorrect-behavior-2119e086393b5640f81cfb4e291ebbdd.gif\",\n        \"alt\": \"错误的行为\",\n        \"title\": \"错误的行为\"\n      }\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    }, `ItemView.body`), ` 进行求值了。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"p\",\n      props: {\n        \"src\": \"/figure-5-breakpoint-for-hack-2-e87b382e8cc91e551ecdaca3188799d9.gif\",\n        \"alt\": \"修改一之后断点行为\",\n        \"title\": \"修改一之后断点行为\"\n      }\n    })), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `这个修改有什么问题？`), React.createElement(MDXTag, {\n      name: \"h2\",\n      components: components\n    }, `修改二：理解 Dynamic Property 更新`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `通过在他的代码中设置断点我们可以发现，即使我们通过增加 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item.seed`), ` 的值来强制要求 SwiftUI 对 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `ItemView.body`), ` 进行求值，`, 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    }, `wrappedValue`), ` 也不会改变。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"figure\",\n      components: components,\n      parentName: \"p\"\n    }, `\n    `, React.createElement(MDXTag, {\n      name: \"a\",\n      components: components,\n      parentName: \"figure\",\n      props: {\n        \"href\": \"/static/ef5dcad1c21508565ce15d439397181e/99f17/figure-6-po-binding.webp\"\n      }\n    }, React.createElement(MDXTag, {\n      name: \"picture\",\n      components: components,\n      parentName: \"a\"\n    }, `\n  `, React.createElement(MDXTag, {\n      name: \"source\",\n      components: components,\n      parentName: \"picture\",\n      props: {\n        \"src\": \"/static/ef5dcad1c21508565ce15d439397181e/0cc25/figure-6-po-binding.png\",\n        \"srcSet\": [\"/static/ef5dcad1c21508565ce15d439397181e/5116e/figure-6-po-binding.png 178w\", \"/static/ef5dcad1c21508565ce15d439397181e/92f55/figure-6-po-binding.png 356w\", \"/static/ef5dcad1c21508565ce15d439397181e/0cc25/figure-6-po-binding.png 712w\", \"/static/ef5dcad1c21508565ce15d439397181e/7ae06/figure-6-po-binding.png 1068w\", \"/static/ef5dcad1c21508565ce15d439397181e/eee47/figure-6-po-binding.png 1424w\", \"/static/ef5dcad1c21508565ce15d439397181e/e8042/figure-6-po-binding.png 1652w\"],\n        \"sizes\": \"(max-width: 712px) 100vw, 712px\"\n      }\n    }), React.createElement(MDXTag, {\n      name: \"source\",\n      components: components,\n      parentName: \"picture\",\n      props: {\n        \"src\": \"/static/ef5dcad1c21508565ce15d439397181e/690c8/figure-6-po-binding.webp\",\n        \"srcSet\": [\"/static/ef5dcad1c21508565ce15d439397181e/25c8a/figure-6-po-binding.webp 178w\", \"/static/ef5dcad1c21508565ce15d439397181e/60698/figure-6-po-binding.webp 356w\", \"/static/ef5dcad1c21508565ce15d439397181e/690c8/figure-6-po-binding.webp 712w\", \"/static/ef5dcad1c21508565ce15d439397181e/d7e52/figure-6-po-binding.webp 1068w\", \"/static/ef5dcad1c21508565ce15d439397181e/456ef/figure-6-po-binding.webp 1424w\", \"/static/ef5dcad1c21508565ce15d439397181e/99f17/figure-6-po-binding.webp 1652w\"],\n        \"sizes\": \"(max-width: 712px) 100vw, 712px\"\n      }\n    }), `\n  `, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"picture\",\n      props: {\n        \"src\": \"/static/ef5dcad1c21508565ce15d439397181e/99f17/figure-6-po-binding.webp\",\n        \"alt\": \"po Binding\",\n        \"title\": \"po Binding\",\n        \"width\": 712,\n        \"height\": 164,\n        \"loading\": \"lazy\"\n      }\n    }))), `\n    `, React.createElement(MDXTag, {\n      name: \"figcaption\",\n      components: components,\n      parentName: \"figure\"\n    }, `\n        `, React.createElement(MDXTag, {\n      name: \"span\",\n      components: components,\n      parentName: \"figcaption\"\n    }, `\n            po Binding\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: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `State`), ` 投影的 `, 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: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `wrappedValue`), ` 会直接返回在 `, 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    }, `get`), ` 闭包的结果。这可以用以下伪代码表示。`), 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    }, `@propertyWrapper\nstruct Binding<Value> {\n\n  let transaction: Transaction\n\n  let getter: () -> Value\n  \n  let setter: (Value, Transaction) -> Void\n  \n  var wrappedValue: Value {\n    get {\n      getter()\n    }\n    set {\n      setter(newValue, transaction)\n    }\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: \"h3\",\n      components: components\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    }, `Binding`), ` 上 bug 一样的行为，我们必须更加深入地了解 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), `。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `在 SwiftUI 中，`, 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`), ` 是 SwiftUI 中受管理动态数据的「引用」。这里的「受管理动态数据」是指能够投影 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 的 dynamic property。SwiftUI 的文档称这些 dynamic property 为「真相源 (Source of Truth)」。这一点可以从 iOS 16 SDK 中随附文档中 `, 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    }, `Binding`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components,\n      parentName: \"blockquote\"\n    }, `A property wrapper type that can read and write a value owned by a source of truth.`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `但是我认为「受管理动态数据」是一个更不容易引发歧义的称呼，因为你必须先让这些 dynamic property 被 SwiftUI 管理了，然后它们才能按照你的期望工作。为了让这些 dynamic property 被 SwiftUI 管理起来，你必须直接将它们安装在 SwiftUI 提供的 `, 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    }, `Gesture`), ` 或其他用户界面结构类型上:`), 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  // 直接安装在 \\`View\\` 上\n  @State\n  var data: Int = 0\n  \n  var body: some View {\n    ...\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    }, `Model.Item`), ` 里面第一个问题是 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item`), ` 的 `, 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    }, `View`), ` 上。`), 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: \"strong\",\n      components: components,\n      parentName: \"p\"\n    }, `受管理动态数据`), `的引用，并且 SwiftUI 实现了事务更新模式来让这些数据在 dependency graph 更新期间保持一致性，因此 `, 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: \"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    }, `在初始化时以及 dependency graph 更新它时缓存最新值`)), 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    }, `当 dependency graph 更新时，返回缓存值`))), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"figure\",\n      components: components,\n      parentName: \"p\"\n    }, `\n    `, React.createElement(MDXTag, {\n      name: \"a\",\n      components: components,\n      parentName: \"figure\",\n      props: {\n        \"href\": \"/static/673b68fe7b16bebd43972e7770e5e730/d5e03/figure-7-binding-update.png\"\n      }\n    }, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"a\",\n      props: {\n        \"title\": \"Binding 更新示意图\",\n        \"alt\": \"Binding 更新示意图\",\n        \"src\": \"/static/673b68fe7b16bebd43972e7770e5e730/d5e03/figure-7-binding-update.png\",\n        \"srcSet\": [\"/static/673b68fe7b16bebd43972e7770e5e730/d5e03/figure-7-binding-update.png 1x\", \"/static/11b5c9d0c3735f67ab688e38a3325491/fd824/figure-7-binding-update%402x.png 2x\", \"/static/28f233658f317bad8a4f990ecd34edea/7d055/figure-7-binding-update%403x.png 3x\"],\n        \"loading\": \"lazy\"\n      }\n    })), `\n    `, React.createElement(MDXTag, {\n      name: \"figcaption\",\n      components: components,\n      parentName: \"figure\"\n    }, `\n        `, React.createElement(MDXTag, {\n      name: \"span\",\n      components: components,\n      parentName: \"figcaption\"\n    }, `\n            Binding 更新示意图\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    }, `Model.Item`), ` 中的第二个问题：当我们在 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `View.body`), ` 求值时读取 `, 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    }, `View.body`), ` 的求值发生在 dependency graph 更新期间，为了遵守事务更新模式 `, 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    }, `Model.Item`), ` 的 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 属性不受 SwiftUI 管理，因此它的缓存值永远不会被更新。 因此，我们可以看到上面类似 bug 一样的行为。`), React.createElement(MDXTag, {\n      name: \"h3\",\n      components: components\n    }, `一个奏效的修改`), 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: \"p\",\n      components: components\n    }, `对于符合 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `DynamicProperty`), ` 协议的第三方定义的类型，在其被安装的 `, 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    }, `body`), ` 被求值之前，其 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `update`), ` 方法会被唤起一次。通过这个 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `update`), ` 方法我们仍然有机会在 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `ItemView.body`), ` 开始求值之前手动「管理」 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item`), ` 上 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 属性的更新。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `因此，我们可以创建一个新的 dynamic property 用来包装 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `@StateObject`), `，并在 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `DynamicProperty.update`), ` 中手动重置 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `@StateObject`), ` 的内容。`), 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\nprotocol Remakable {\n  \n  func remake()\n  \n}\n\n@propertyWrapper\nstruct RemakableStateObject<ObjectType: ObservableObject & Remakable>: DynamicProperty {\n  \n  var wrappedValue: StateObject<ObjectType>\n  \n  func update() {\n    wrappedValue.wrappedValue.remake()\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    }, `Model`), ` 以符合我们刚刚声明的 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Remakable`), ` 协议了。`), 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    }, `class Model: Remakable, ObservableObject {\n  \n  struct Item {\n    \n    var description: String\n    \n    var binding: Binding<Bool>\n    \n  }\n  \n  var items: [Item] = []\n  \n  init() {\n    remake()\n  }\n  \n  func remake() {\n    items = [\n     Item(description: \"Test\", isOn: Binding(get: {\n       LegacyModel.shared.isOn\n     }, set: { [unowned self] value in\n       LegacyModel.shared.setOn(value) { _ in\n         DispatchQueue.main.async {\n           self.objectWillChange.send()\n         }\n       }\n     }))\n   ]\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    }, `@StateObject var model: Model`), ` 添加一个额外的 property wrapper -- `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `@RemakableStateObject`), ` 才能让代码正常工作。`), 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  @RemakableStateObject\n  @StateObject\n  var model = Model()\n  \n  ...\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `现在我们可以通过点击「OK」按钮来使开关切换到开启状态了。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"p\",\n      props: {\n        \"src\": \"/figure-1-expected-behavior-b48113f518a82b5a37b3552e66d526ee.gif\",\n        \"alt\": \"修改二之后的行为\",\n        \"title\": \"修改二之后的行为\"\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: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 来包装引用语义模型，因为他将包装的引用语义模型视为「唯一真相源」。这是对 SwiftUI 中这个概念的误解。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `通过上述解释，我们已经知道 SwiftUI 中合格的「真相源」是安装在 `, 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    }, `Gesture`), ` 或其他结构性用户界面类型上的 dynamic property。这是将引用语义模型迁移到 SwiftUI 世界的最简单、最恰当的方式。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `对于我同事的情况，我们依然可以让模型遵从 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `ObservableObject`), `，但是我们必须把 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `isOn`), ` 改成普通的 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Bool`), ` 类型。`), 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    }, `class Model: ObservableObject {\n\n  struct Item {\n  \n    let description: String\n    \n    let isOn: Bool\n  \n  }\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `那么我们如何处理引用语义模型中的异步 setter 呢？解决方案是：在异步 setter 完成工作后，我们需要使用「引用」来设置 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `@StateObject var model: Model`), ` 中 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item`), ` 的新值。`), 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    }, `首先，我们需要在异步 setter 完成后获取要修改的数据的 `, 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    }, `ItemView`), ` 中调用 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `$item`), ` 来获得 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item`), ` 的 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), `。`), 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 ItemView: View {\n    \n  @Binding\n  var item: Model\n    \n  @State\n  var needsUserConfirm: Bool = false\n    \n  var body: some View {\n    Button(item.description) {\n      if item.isOn {\n        // \\`setOn\\` 封装了对异步 setter 的调用\n        item.setOn($item, false)\n      } else {\n        needsUserConfirm = true\n      }\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    }, `Model.Item`), ` 上声明一个 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `setOn`), ` 闭包。这个闭包的参数接收：`), 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    }, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item`), ` 的 `, 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: \"p\",\n      components: components\n    }, `这样我们就可以在其中填上可定制的同步/异步 setter 调用逻辑了。`), 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    }, `class Model: ObservableObject {\n\n  struct Item {\n    \n    var description: String\n    \n    fileprivate(set) var isOn: Bool\n    \n    var setOn: (_ item: Binding<Item>, _ isOn: Bool) -> Void\n    \n  }\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `对于我的同事的情况，当异步 setter 完成时，我们应该拿到 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item`), ` 的 `, 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: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `wrappedValue`), ` 中。`), 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    }, `class Model: ObservableObject {\n\n  ...\n\n  @Published\n  var items: [Item] = []\n  \n  init() {\n    items = [\n      Item(\n        description: \"Test\",\n        isOn: LegacyModel.shared.isOn,\n        setOn: { (item, newValue) in\n          LegacyModel.shared.setOn(newValue) { success in\n            guard success else {\n              return\n            }\n            DispatchQueue.main.async {\n              item.wrappedValue.isOn = newValue\n            }\n          }\n        }\n      )\n    ]\n  }\n  \n}\n`)), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, React.createElement(MDXTag, {\n      name: \"img\",\n      components: components,\n      parentName: \"p\",\n      props: {\n        \"src\": \"/figure-1-expected-behavior-b48113f518a82b5a37b3552e66d526ee.gif\",\n        \"alt\": \"合格真相源的行为\",\n        \"title\": \"合格真相源的行为\"\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    }, `self.objectWillChange.send()`), `。这是因为 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Model.Item`), ` 的 `, 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    }, `@StateObject var model: Model`), ` 的变更传播 ———— 对 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `@StateObject var model: Model`), ` 的变更帮你调用了 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `self.objectWillChange.send()`)), React.createElement(MDXTag, {\n      name: \"h2\",\n      components: components\n    }, `想要更好的解决方案？关注后续文章`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `尽管我们已经迭代了 3 个版本的解决方案，但最终解决方案仍不够理想。我们可以看到：`), 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    }, `用户互动上，具备 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Button`), ` 行为的 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Toggle`), ` 操作起来有点怪异。在这个实现中某些 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Toggle`), ` 的用户互动已经不见了。`)), 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    }, `当异步 setter 完成后，`, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Toggle`), ` 的关闭动画才开始。这意味着用户可能在真正将注意力从 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Toggle`), ` 上移走之前一直会等待这个 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Toggle`), ` 的关闭动画 ———— 这对于即将失去用户关注焦点的事物而言这不是一个好的设计。如果 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Toggle`), ` 能即刻响应关闭，并在失败发生时重置回已开启的状态，那就会更好。`)), 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    }, `Item.Model.setOn`), ` 中手动设置异步 setter 的逻辑，这很容易出错。`)), 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    }, `如果引用语义模型提供异步 getter 我们该如何处理？`)), 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: \"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    }, `ItemView`), ` 中 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Button`), ` 的 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `action`), ` 闭包中有可以提取的重复代码`))), 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    }, `只有 getter 方法，没有 setter 方法。`)), 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    }, `只有 setter 方法，没有 getter 方法。`)), 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 之外被修改而没有任何通知。`))), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\n    }, `要处理上述符合情况的引用语义模型，我们需要写更多的代码来让 SwiftUI 中的真相源可以正常工作。`), React.createElement(MDXTag, {\n      name: \"p\",\n      components: components\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: \"p\",\n      components: components,\n      parentName: \"li\"\n    }, `SwiftUI 中的真相源是可以投影 `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Binding`), ` 的 dynamic property`)), 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    }, `View`), `, `, React.createElement(MDXTag, {\n      name: \"inlineCode\",\n      components: components,\n      parentName: \"p\"\n    }, `Gesture`), ` 或者 SwiftUI 中其他结构性用户界面类型上直接安装 dynamic property`)), 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 中简单地处理异步方法。`))));\n  }\n\n}\nMDXContent.isMDXComponent = true;","scope":""},"headings":[{"value":"介绍","depth":2},{"value":"最开始的问题","depth":2},{"value":"修改一：什么在驱动 SwiftUI 的更新？","depth":2},{"value":"修改二：理解 Dynamic Property 更新","depth":2},{"value":"深入理解 Binding","depth":3},{"value":"一个奏效的修改","depth":3},{"value":"建议解决方案：构建一个合格的真相源","depth":2},{"value":"想要更好的解决方案？关注后续文章","depth":2},{"value":"结论","depth":2}]}}},"earlierPostExcerpt":{"slug":"/post/2022/08/using-functional-binding-to-observe-in-swiftui-19a8","title":"在 SwiftUI 中使用函数式 Binding 实现观察者模式","subtitle":"","createdTime":"2022-08-19T00:00:00.000Z","tags":["SwiftUI","Binding","Swift","观察者模式"],"category":"编程","file":{"childMdx":{"excerpt":"故事 这周，我的同事问了我一个问题：在 SwiftUI 中怎么观察用户对  Picker  的选择行为？ 这是一个来自真实业务的问题，所以我觉得值得我花费时间去解决它。 范例代码如下所示，然后我的同事想观察用户对  Picker  候选项的选择行为。 分析 但是，「观察」本身的意义可能会随着上下文变动而变动： 它可以表示用户在  Picker  上放下手指的那一刻。 它可以表示用户在  Picker  上抬起手指的那一刻。 它可以表示  Picker  对  $selection  进行值变更的那一刻。 上述每一项都将导致不同的最终解决方案。 因为 SwiftUI 控件可以使用 style…"}}},"laterPostExcerpt":{"slug":"/post/2023/08/swift-macro-revisited-the-strengths-and-essence-a5a4","title":"再探 Swift 宏 - 优势与本质","subtitle":"","createdTime":"2023-08-08T00:00:00.000Z","tags":["Swift","宏"],"category":"编程","file":{"childMdx":{"excerpt":"通过 WWDC 2023，我们了解到 Swift 宏的目标是： 消除重复代码 让繁琐的事情变得简单 以软件包的形式与其他开发者分享 然而，这些目标并不是 Swift 宏所独有的。对于 Swift 中的许多代码重用手段而言，这些都是共同的目标，如函数、类型和模块。可以说，所有高级编程语言都希望实现这些目标。Swift 宏必须在其他方面有所胜出；否则的话没必要存在。 那么， Swift 宏到底擅长什么？ 这个问题的答案至关重要。 Swift 宏独一无二的优势会决定它的本质：这可以指导我们创作有效的 Swift 宏、告诉我们在创作宏时的行为边界，并最终帮助我们产出良好设计的 Swift…"}}}},"pageContext":{"postId":"50422053-00c8-522e-a118-2c12b7f4d13f","earlierPostId":"5c75555f-38a8-50fe-b14c-454701f208be","laterPostId":"bfebcd5e-37cf-5ace-be71-d0ea1b5d8e48"}}