SwiftUI中@State和@StateObject如何确保View重建前后数据一致

我们知道在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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值