@State 研究
如想获得更好的阅读体验,可以访问我的博客 www.fatbobman.com。
本文试图探讨并分析SwiftUI 中 @State的实现方式和运行特征;最后提供了一个有关扩展@State功能的思路及例程。读者需要对SwiftUI的响应式编程有基本概念。
研究的意义何在
我在去年底使用了SwiftUI写了第一个 iOS app 健康笔记,这是我第一次接触响应式编程概念。在有了些基本的认识和尝试后,深深的被这种编程的思路所打动。不过,我在使用中也发现了一些奇怪的问题。我发现在视图(View)数量达到一定程度,随着数据量的增加,整个app的响应有些开始迟钝,变得有粘滞感、不跟手。app响应出现了问题一方面肯定和我的代码效率、数据结构设计欠佳有关;不过随着继续分析,发现其中也有很大部分原因来自于SwiftUI中所使用的响应式的实现方式。不恰当的使用,可能导致响应速度会随着数据量及View量的增加而大幅下降。通过一段时间的研究和分析,我打算用两篇文章来阐述这方面的问题,并尝试提供一个现阶段的使用思路。
数据(状态)驱动
在SwiftUI中,视图是由数据(状态)驱动的。按照苹果的说法,视图是状态的函数,而不是事件的序列(The views are a function of state, not a sequence of events)。每当视图在创建或解析时,都会为该视图和与该视图中使用的状态数据之间创建一个依赖关系,每当状态的信息发生变化时,有依赖关系的视图则会马上反应出这些变化并重绘。SwiftUI中提供了诸如 @State ObservedObject EnvironmentObject等来创建应对不同类型、不同作用域的状态形式。
类型及作用域图片来自于SwiftUI for Absoloute Beginners
其中@State只能用于当前视图,并且其对应的数据类型为值类型(如果非要对应引用类型的话则必须在每次赋值时重新创建新的实例才可以)。
代码语言:javascript复制struct DemoView:View{ @State var name = "肘子" var body:some View{ VStack{ Text(name) Button("改名"){ self.name = "大肘子" } } }}
通过执行上面代码,我们可以发现两个情况:
1.通过使用@State,我们可以在未使用mutating的情况下修改结构中的值2.当状态值发生变化后,视图会自动重绘以反应状态的变化。
@State如何工作的
在分析@State如何工作之前,我们需要先了解几个知识点
属性包装器
作为swift 5.1的新增功能之一,属性包装器在管理属性如何存储和定义属性的代码之间添加了一个分割层。通过该特性,可以在对值校验、持久化、编解码等多个方面获得收益。
它的实现也很简单,下面的例子定义了一个包装器用来确保它包装的值始终小于等于12。如果要求它存储一个更大的数字,它则会存储 12 这个数字。呈现值(投射值)则返回当前包装值是否为偶数
代码语言:javascript复制@propertyWrapperstruct TwelveOrLess { private var number: Int init() { self.number = 0 } var wrappedValue: Int { get { return number } set { number = min(newValue, 12) } } var projectedValue: Bool { self.number % 2 == 0 }}
更多的具体资料请查阅官方文档
Binding
Binding是数据的一级引用,在SwiftUI中作为数据(状态)双向绑定的桥梁,允许在不拥有数据的情况下对数据进行读写操作。我们可以绑定到多种类型,包括 State ObservedObject 等,甚至还可以绑定到另一个Binding上面。Binding本身就是一个Getter和Setter的封装。
State 的定义
代码语言:javascript复制@frozen @propertyWrapper public struct State<Value> : DynamicProperty { /// Initialize with the provided initial value. public init(wrappedValue value: Value) /// Initialize with the provided initial value. public init(initialValue value: Value) /// The current state value. public var wrappedValue: Value { get nonmutating set } /// Produces the binding referencing this state value public var projectedValue: Binding<Value> { get }}
DynamicProperty 的定义
代码语言:javascript复制public protocol DynamicProperty { /// Called immediately before the view's body() function is /// executed, after updating the values of any dynamic properties /// stored in `self`. mutating func update()}
工作原理
前面我们说过 @State 有两个作用
1.通过使用@State,我们可以在未使用mutating的情况下修改结构中的值2.当状态值发生变化后,视图会自动重绘以反应状态的变化。
让我们根据上面的知识点来分析如何才能实现以上功能。
•@State本身包含 @propertyWrapper,意味着他是一个属性包装器。•public var wrappedValue: Value { get nonmutating set } 意味着他的包装值并没有保存在本地。•它的呈现值(投射值)为Binding类型。也就是只是一个管道,对包装数据的引用•遵循 DynamicProperty 协议,该协议完成了创建数据(状态)和视图的依赖操作所需接口。现在只暴露了很少的接口,我们暂时无法完全使用它。
在了解了以上几点后,我们来尝试使用自己的代码来构建一个@State的半成品
代码语言:javascript复制@propertyWrapperstruct MyStates:DynamicProperty{ init(wrappedValue:String){ UserDefaults.standard.set(wrappedValue, forKey: "myString") } var wrappedValue:String{ nonmutating set{UserDefaults.standard.set(newValue, forKey: "myString")} get{UserDefaults.standard.string(forKey: "myString") ?? ""} } var projectedValue:Binding<String>{ Binding<String>( get:{String(self.wrappedValue)}, set:{ self.wrappedValue = $0 } ) } func update() { print("重绘视图") }}
这是一个可以用来包装String类型的State。
我们使用UserDefault将数据包装后保存到本地。读取包装数据也是从本地的UserDefault里读取的。
为了能够包装其他的类型的数据,同时也为了能够提高存储效率,进一步的可以修改成如下代码:
代码语言:javascript复制@propertyWrapperstruct MyState<Value>:DynamicProperty{ private var _value:Value private var _location:AnyLocation<Value>? init(wrappedValue:Value){ self._value = wrappedValue self._location = AnyLocation(value: wrappedValue) } var wrappedValue:Value{ get{ _location?._value.pointee ?? _value} nonmutating set{ _location?._value.pointee = newValue} } var projectedValue:Binding<Value>{ Binding<Value>( get:{self.wrappedValue}, set:{self._location?._value.pointee = $0} ) } func update() { print("重绘视图") }}class AnyLocation<Value>{ let _value = UnsafeMutablePointer<Value>.allocate(capacity: 1) init(value:Value){ self._value.pointee = value }}
至此,我们完成了这个@MyState的半成品。
之所以说是半成品,因为尽管我们也遵循了DynamicProperty协议,但我们自己编写的这段代码并不能和视图建立依赖。我们可以和使用@State一样来使用@MyState,同样支持绑定、修改,除了视图不会自动刷新