薛定谔的 @State
2020/5/28 23:26:45
本文主要是介绍薛定谔的 @State,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
目录
- @State,@Published, @ObservedObject,等等
- 根据 projectedValue 进行分类
- 薛定谔的 @State
- 幽灵般的状态更新
译自 nalexn.github.io/stranger-th…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
同许多开发者一样,我对于 SwiftUI 的了解是从苹果官方的精彩教程开始的。然而,这个开局姿势也给我灌输了一个 “SwiftUI 极其易学” 的错误观念。
在那之后,SwiftUI 的众多充满趣味和技巧的主题强烈地吸引了我。想要快速地搞清楚它们是一件有挑战的事情。即便是一些有经验的开发者,也说学习 SwiftUI 的过程就像从头开始学习一切。
在这篇文章中,我收集了跟状态管理相关的我个人对 SwiftUI 最为困惑的一些方面。可以说,假如我自己有这篇文章在手,可以省去数以小时计的解决问题的痛苦经历。
让我们开始吧!
@State,@Published, @ObservedObject,等等
一开始,我对这些 @Something
的认知是:它们是一组崭新的语言属性,就像 weak
或者 lazy
,只不过是专门为 SwiftUI 引入的。
因此,我很快就因为这些新的“关键词”可以基于前缀产生各种变体而感到困惑:
value
,$value
和_value
代表三个完全不同的东西!
我不知道,这几个 @Things
其实只是 SwiftUI 框架中的几个结构体,并非 Swift 语言的一部分。
而真正属于语言的一部分的是 Swift 5.1 引入的一个新特性:属性包装器.
在阅读了关于属性包装器的文档之后,我才恍然大悟,@State
或者 @Published
的背后其实没有秘密。正是这些包装器给原始变量赋予了“超能力”,比如 @State
的不可变性和可变形,@Published
的响应式能力。
听完之后更疑惑了吗?不用担心 —— 我立刻给你解释。
事情的全貌其实很清晰:当我们用 SwiftUI 里的 @Something
给变量标注属性时,比如 @State var value: Int = 0
,Swift 编译器将为我们生成三个变量!(其中有两个是计算属性):
value
—— 被包装的由我们声明类型的原始值(wrappedValue
),比如例子中的 Int
。
$value
—— 一个 “额外的” projectedValue
,它的类型由我们使用的属性包装器决定。@State
的projectedValue
的类型是 Binding
,因此我们的例子中就是 Binding
类型。
_value
—— 属性包装器本身的引用,在视图初始化过程中可能用到:
struct MyView: View { @Binding var flag: Bool init(flag: Binding<Bool>) { self._flag = flag } }复制代码
根据 projectedValue 进行分类
让我们浏览一下 SwiftUI 中最常用的 @Things
,看看他们的 projectedValue
分别都是些什么:
@State
——Binding<Value>
@Binding
——Binding<Value>
@ObservedObject
——Binding<Value>
(*)@EnvironmentObject
-Binding<Value>
(*)@Published
-Publisher<Value, Never>
技术上来讲,(*) 给到我们的是 Wrapper
类型的中间值,一旦我们为该对象中的实际值指定了 keyPath,就会变成一个 Binding
。
如你所见,SwiftUI 中大部分的属性包装器,其职能都是跟视图的状态有关,并且被投射为 Binding
,用于在视图之间传递状态。
唯一的跟大多数包装器不同的是 @Published
,不过请注意:
- 它是在 Combine 框架而不是 SwiftUI 里声明的
- 它的用途是让值变为可观察的
- 它不用于视图的变量声明,只用在
ObservableObject
内部。
考虑一个在 SwiftUI 中相当常见的场景:声明一个 ObservableObject
,并在某个视图中以 @ObservedObject
属性使用它:
class ViewModel: ObservableObject { @Published var value: Int = 0 } struct MyView: View { @ObservedObject var viewModel = ViewModel() var body: some View { ... } }复制代码
MyView
可以引用 $viewModel.value
和 viewModel.$value
—— 两个表达式都是合法的。有点犯迷糊了是不是?
其实这两个表达式分别代表了完全不同的两个类型:Binding
和 Publisher
。
两者都有实际的用途:
var body: some View { OtherView(binding: $viewModel.value) // Binding .onReceive(viewModel.$value) { value // Publisher // 执行某些不需要视图更新的操作 } }复制代码
薛定谔的 @State
我们都知道包含在一个不可变的 struct
内部的 struct
也是不可变的。
在 SwiftUI 中,多数情况下我们面对是一个不可修改的 self
,例如,在某个 Button
的回调中。基于这种上下文,每个实例变量,包括 @State
结构体也都是不可变的。
那么,你能解释一下为什么下面的代码是完全合法的吗?
struct MyView: View { @State var counter: Int = 0 var body: some View { Button(action: { self.counter += 1 // 修改一个不可变的结构体! }, label: { Text("Tap me!") }) } }复制代码
尽管是不可变的结构体,我们还是可以修改它的值,@State
有什么魔法?
这里有一份关于 SwiftUI 如何处理这种场景下的值的变化的详细解释,但这里我想强调一个事实:对于 @State
变量实际的值,SwiftUI 使用了隐藏的外部存储。
@State
其实是一个代理:它拥有一个内部变量 _location
,用于访问外部存储。
让我给你出道面试题:下面这个例子会打印出什么内容?
func test() { var view = MyView() view.counter = 10 print("\(view.counter)") }复制代码
上面的代码相当直观;直觉告诉我们打印的值应该是 10。
然而并不是 —— 输出是 0。
这其中的玄机在于视图并非总是同状态存储连接:SwiftUI 会在视图需要重绘或者视图接收来自 SwiftUI 的回调的时候接通连接,而在之后又断开。
与此同时,在 DispatchQueue.main.async
中对 State
做出的修改将不能保证成功:某些时候可能是工作的。但假如你引入某个延迟,而存储连接在闭包执行时已经被断开了,那么状态修改就不会生效了。
对于 SwiftUI 视图来说,传统的异步分发是不安全的 —— 不要引火烧身。
幽灵般的状态更新
在用了多年的 RxSwift 和 ReactiveSwift 之后,对于数据流通过响应式绑定和视图的属性建立连接这件事,我认为是理所当然的。
但是当我尝试将 SwiftUI 和 Combine 放在一起协作的时候,我震惊了。
这两个框架之间表现得相当异质:一方并不能很轻松地把某个 Publisher
连接到某个 Binding
,或者把某个 CurrentValueSubject
转换成 ObservableObject
。
两种框架之间互操作的方式只有几种。
第一个接触点是 ObservableObject
—— 它是一个声明在 Combine 里的协议,但已经广泛地用于 SwiftUI 的视图。
第二个是 .onReceive()
视图 modifier,它是让你将视图和任意数据连接的唯一 API。
我的下一个大大的疑惑正是和这个 modifier 有关。看一下这个例子:
struct MyView: View { let publisher: AnyPublisher<String, Never> @State var text: String = "" @State var didAppear: Bool = false var body: some View { Text(text) .onAppear { self.didAppear = true } .onReceive(publisher) { print("onReceive") self.text = $0 } } }复制代码
这是视图只是显示了由 Publisher
生产的字符串,并且在视图出现在屏幕时设置 didAppear
标记 ,就这么简单而已。
现在,试着回答我,你认为在下面这两个用例中,print("onReceive")
会被触发几次?
struct TestView: View { let publisher = PassthroughSubject<String, Never>() // 1 let publisher = CurrentValueSubject<String, Never>("") // 2 var body: some View { MyView(publisher: publisher.eraseToAnyPublisher()) } }复制代码
让我们先考虑 PassthroughSubject
。
如果你的答案是 0,那么恭喜你,回答正确。PassthroughSubject
从未接收到任何值,因此没有东西会被提交到 onReceive
闭包。
第二用例有一点欺骗性。请认真点,仔细分析其中的猫腻。
当试图被创建时,onReceive
modifier 将订阅 Publisher
,提供无限制的值“要求” (参考 Combine 中的说明)。
由于 CurrentValueSubject
拥有初始值 ""
,它会立即将值推送给它的新订阅者,触发 onReceive
回调。
然后,当视图即将第一次显示在屏幕上时,SwiftUI 会调用它的 onAppear
回调,在我们的例子,这个回调会通过设置 didAppear
为 true
来修改视图的状态。
那么接下来会发生什么? 你猜的没错!onReceive
闭包再次调用了!为什么会这样?
当 MyView
修改 onAppear
中的状态时,SwiftUI 需要创建一个新的视图,以便和状态改变之前的视图做对比! 这是给视图层级打上合适的补丁所要求的步骤。
由于第二次创建过程的视图也订阅了 Publisher
,后者欢欢喜喜地又推送了自己的值。
正确答案是 2。
你能想象我在调试这些被传递给 onReceive
的幽灵般的更新调用时的困惑吗?当我试图去过滤掉这些重复的更新调用时,我的脑门上挂满了问号。
最后一个测验:如果我们在 onAppear
里设置 self.text = "abc"
,那最后会显示什么文本?
如果你不知道上面这个故事,那合乎逻辑的答案应当是 “abc”,但是当你已经用新知识升级了自己:无论何时何地你给 text
赋值,onReceive
回调都会如影随形,用 CurrentValueSubject
的值擦掉你刚刚赋的值。
我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~
这篇关于薛定谔的 @State的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值