我们知道在SwiftUI中,如果修改了@State属性包装器修饰的值类型变量,会引起View自身的重新渲染,也就是调用body方法。此时该View的struct是不会重新创建的,但是body方法里的各种子View会被重新创建。举个例子:
import SwiftUI
class StateObjectClass:ObservableObject{
let type:String
let id:Int
@Published var count = 0
init(type:String){
self.type = type
self.id = Int.random(in: 0...1000)
print("type:\(type) id:\(id) init")
}
deinit {
print("type:\(type) id:\(id) deinit")
}
}
struct StateStruct{
let type:String
let id:Int
var count = 0
init(type:String){
self.type = type
self.id = Int.random(in: 0...1000)
print("type:\(type) id:\(id) init")
}
}
struct CountViewStateStruct:View{
@State var state = StateStruct(type:"StateStruct")
init() {
print("CountViewStateStruct init")
}
var body: some View{
VStack{
Text("@State count :\(state.count)")
Button("+1"){
state.count += 1
}
}
}
}
struct CountViewState:View{
@StateObject var state = StateObjectClass(type:"StateObject")
init() {
print("CountViewState init")
}
var body: some View{
VStack{
Text("@StateObject count :\(state.count)")
Button("+1"){
state.count += 1
}
}
}
}
struct CountViewObserved:View{
@ObservedObject var state = StateObjectClass(type:"Observed")
init() {
print("CountViewObserved init")
}
var body: some View{
VStack{
Text("@Observed count :\(state.count)")
Button("+1"){
state.count += 1
}
}
}
}
struct Test1: View {
@State var count = 0
var body: some View {
VStack{
Text("刷新CounterView计数 :\(count)")
Button("刷新"){
count += 1
}
CountViewStateStruct()
.padding()
CountViewState()
.padding()
CountViewObserved()
.padding()
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
Test1()
}
}
我们发现点击“+1”Button,都能正确增加和显示点击次数。因为这三个子View都只是调用body进行自身更新,其持有的state都没重新创建。
但是点击“刷新”Button后,CountViewObserved的计数被重置了。查看打印结果:
type:StateStruct id:860 init
CountViewStateStruct init
CountViewState init
type:Observed id:938 init
CountViewObserved init
type:Observed id:490 deinit
我们发现三个子View都重新创建了,CountViewStateStruct的state也进行了重建,但是保持了上次的值,说明系统会在View重建后,恢复上次的值。
CountViewState进行了重建,但是没有任何StateObjectClass创建和销毁的信息。
CountViewObserved进行了重建,StateObjectClass也进行了销毁和创建,所以计数被重置。
首先我们来看@State的原型:
struct State<Value> : DynamicProperty {
init(wrappedValue value: Value)
init(initialValue value: Value)
var wrappedValue: Value { get nonmutating set }
var projectedValue: Binding<Value> { get }
}
代入到我们的例子,就会产生类似如下代码:
private var _state: State<StateStruct> = State(initialValue: StateStruct(type:"StateStruct"))
private var $state: Binding<StateStruct> { return _state.projectedValue }
private var state: StateStruct {
get { return _state.wrappedValue }
nonmutating set { _state.wrappedValue = newValue }
}
那么CountViewStateStruct每次重建的时候,都会调用State(initialValue: StateStruct(type:"StateStruct")),因此StateStruct的init方法会被调用,符合我们的预期。而调用body时显示了上次的值,表明在重建和调用body之间有一个值恢复的过程。
而对于StateObjectClass并没有特殊处理,所以能看到StateObjectClass创建和销毁的过程。
CountViewState和CountViewStateStruct类似,View都重建了,值都保持了,差别是没有看到StateObjectClass的创建和销毁。为什么呢?我们来看看@StateObject的原型:
@frozen @propertyWrapper public struct StateObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
@inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
public var wrappedValue: ObjectType { get }
public var projectedValue: ObservedObject<ObjectType>.Wrapper { get }
}
和上面@State进行比较,我们发现它没有 init(initialValue value: Value)这个方法。试想下,如果@StateObject中也是使用 init(initialValue value: Value),那么肯定每次会调用StateObjectClass的init的方法。如何确保只在第一次在堆上创建对象,存储其引用,并在下次View重建的时候仍然保持这个引用,并且不创建新的对象?我想了想,似乎只有在@State基础上,配合自动闭包能够实现。而定义中,public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)这个方法,它的参数的确是一个自动闭包!它的实现可能是这样,在State中检查是否有对应的值,初次没有值的时候,就执行闭包,并且保存返回的引用。下次View重建的时候,也是在State中检查是否有对应的值,因为已经存在,所以不执行闭包。这样我们代码中:
@StateObject var state = StateObjectClass(type:"StateObject")
创建对象的赋值语句就不会执行,也就避免了对象的创建,同时类似@State,在body执行前恢复了View中的这个引用值。
拓展阅读:
SwiftUI 编程指南https://www.sohu.com/a/411890385_470093
关于 SwiftUI State 的一些细节https://onevcat.com/2021/01/swiftui-state/
@State 研究 https://zhuanlan.zhihu.com/p/141229504
SwiftUI 2.0 —— @StateObject 研究 https://zhuanlan.zhihu.com/p/151286558
ObservableObject研究——想说爱你不容易 https://zhuanlan.zhihu.com/p/141434179