onevcat / OneV-s-Den-Comments

0 stars 0 forks source link

2020/06/stateobject/ #9

Open utterances-bot opened 3 years ago

utterances-bot commented 3 years ago

@StateObject 和 @ObservedObject 的区别和使用 | OneV's Den

WWDC 2020 中,SwiftUI 迎来了非常多的变更。相比于 2019 年的初版,可以说 SwiftUI 达到了一个相对可用的状态。从这篇文章开始,我打算写几篇文章来介绍一些重要的变化和新追加的内容。如果你需要 SwiftUI 的入门和基本概念的材料,我参与的两本书籍《SwiftUI 与 Combine 编程》和

https://onevcat.com/2020/06/stateobject/

wanhmr commented 3 years ago

如果不希望 model 状态在 View 刷新时丢失,那确实可以进行替换,这 (虽然可能会对性能有一些影响,但) 不会影响整体的行为。

有点奇怪,已经说了 model 不会重复,为什么会说可能会对性能有影响

这个 session 26m 的时候也提到了,在 view 频繁创建的场景提倡使用 @StateObject。

个人的一点理解来看,@StateObject 生命周期与真实视图(UIKit 中)一致,而 @ObservedObject 生命周期与虚拟视图(SwiftUI 中)一致。

dongdongya commented 3 years ago

喵大,为什么上面的例子中当 niceScore 是4的时候,Nice? 还是显示 NO,您的代码中写的是 if model.score > 3 的时候 niceScore 已经为 ture 了呀

wanhmr commented 3 years ago

喵大,为什么上面的例子中当 niceScore 是4的时候,Nice? 还是显示 NO,您的代码中写的是 if model.score > 3 的时候 niceScore 已经为 ture 了呀

如果你仔细看下源码:

Button("+1") {
    if model.score > 3 {
          niceScore = true
      }
      model.score += 1
}

可以看到 score 是后加的。因此你说的场景,当 score 为 3 时,点击 button,此时 model.score > 3 的判断并不会进入。

PS: 由于有邮件提醒,因此我回复你一下。 @dongdongya

dongdongya commented 3 years ago

好的,谢谢喵大,抱歉是我没有认真思考

Mamong commented 3 years ago

@State或者@StateObject是如何来确保在View的生命周期期间只创建一次的?

monsoir commented 3 years ago

倘若想:在 @StateObject 修饰的属性初始化时,进行某些数据的注入。 但由于 @StateObject 修饰的属性变成了只读,一般情况下只能使用默认的无参数初始化。

在网上找了一圈,发现都是建议在 View 的 onAppear 时再进行数据的“真正初始化”。

不知道上述的初始化注入,在 SwiftUI 中是否属于反模式呢?又或者有其他我还没有领悟到的做法?

搭个楼请教一下喵大和各位,谢谢。

onevcat commented 3 years ago

@Monsoir 这个其实在另外一篇文章里也有一点涉及

https://onevcat.com/2021/01/swiftui-state/

我个人的认知上来说,可能可以总结几点:

  1. 这种传递 @State 或者 @StateObject 的方式,是反模式的。虽然通过一些技巧可以实现 (比如 onAppear 时的赋值),但是这种时候你需要确保你真的理解 ViewonAppear 或者 init 的生命周期,而且“长期”关注它在今后可能的变化,因为这可能会破坏最初的假设。
  2. 因为 1,虽然 Apple 只说了“推荐将 @State 标记为 private”,但实际上其实 @StateObject 应该也需要是 private。
  3. 要进行传递或者注入,其实无论如何还是涉及外界了。大部分情况下,选择 @Binding@ObservedObject 会更正确。

如果一定需要进行所谓的“注入”,暂时我也没发现比 onAppear 更好的方法。不过可能注意一下,不要把整个 @StateObject 做注入,而只是保持 private 和声明 @StateObject 时直接赋值,然后在 init 中只传递需要初始化的那些数据,并在 onAppear 时设置这些数据 (甚至可以判断一下是不是已经初始化过了,因为这个时候就算是意外调用,@StateObject 也还是原来的那个),会是更安全的做法。

onevcat commented 3 years ago

@Mamong

虽然不太准确,但是你可以理解为内部维护了一个字典,来维持“显示的 View” -> “数据存储“的映射。这个的话今年 WWDC 的 SwiftUI 解密的 session 总结了不少。https://developer.apple.com/videos/play/wwdc2021/10022/

k7arm commented 2 years ago

如果在ScorePlate的View中添加一个Button("xxx"){showRealName2.toggle()}, @ObservedObject var model = Model() 然后进入这个页面的时候点击”xxx"按钮,为什么没有没有把score的值清零呢? 代码

struct ScorePlate: View {
    @State private var showRealName2 = false
    @ObservedObject var model = Model()
    @State private var niceScore = false

    var body: some View {
        VStack {
            Button("+1") {
                if model.score > 3 {
                    niceScore = true
                }
                model.score += 1
            }
            Text("Score: \(model.score)")
            Text("Nice? \(niceScore ? "YES" : "NO")")
            Button("Toggle Name") {
                showRealName2.toggle()
            }
            ScoreText(model: model).padding(.top, 20)
        }
    }
}

点击xxx按钮没有引起这个页面的View刷新吗?

k7arm commented 2 years ago

@onevcat

k7arm commented 2 years ago

struct ContentView: View {
    @State private var showRealName = false
    var body: some View {
        NavigationView{
        VStack {
            Button("Toggle Name") {
                showRealName.toggle()
            }
            Text("Current User: \(showRealName ? "Wei Wang" : "onevcat")")
            //ScorePlate().padding(.top, 20)
            NavigationLink("Next", destination: ScorePlate().padding(.top, 20))
            }
        }
    }
}

class Model: ObservableObject {
    init() { print("Model Created") }
    @Published var score: Int = 0
}
struct ScorePlate: View {
    @State private var showRealName2 = false
    @ObservedObject var model = Model()
    @State private var niceScore = false

    var body: some View {
        VStack {
            Button("+1") {
                if model.score > 3 {
                    niceScore = true
                }
                model.score += 1
            }
            Text("Score: \(model.score)")
            Text("Nice? \(niceScore ? "YES" : "NO")")
            Button("Toggle Name") {
                showRealName2.toggle()
            }
            ScoreText(model: model).padding(.top, 20)
        }
    }
}

struct ScoreText: View {
    @ObservedObject var model: Model

    var body: some View {
        if model.score > 10 {
            return Text("Fantastic")
        } else if model.score > 3 {
            return Text("Good")
        } else {
            return Text("Ummmm...")
        }
    }
}
SunsetWan commented 2 years ago

@k7arm showRealName2 这个 state 的变化没有导致 body 的变化,所以 UI 不会更新。

ximmyxiao commented 10 months ago

如果 View 本身就期望每次刷新时获得一个全新的状态,那么对于那些不是自己创建的,而是从外界接受的 ObservableObject 来说,@StateObject 反而是不合适的。

我试了下,貌似如果 改成外部传入 model

     ScorePlate(model: model).padding(.top, 20)

不管里面ScorePlate是用 @StateObject var model:Model 还是 @ObservedObject var model:Model ,效果都一样。所以如果是获取一个全新的状态是不是不能采用这种外面传入model的形式? 另外请问下假如外面使用@StateObject创建的model,传到子view里面又用@StateObject 声明了一次,这两次会导致创建两块存储么?还是两次@StateObject实际上指向的同一个内存?

chensifang commented 7 months ago

使用改为 @StateObject 之前的代码的确能看到 Model 在不断 init,但奇怪的是没有 deinit。似乎这些 Model 都没有被释放,不知道这是什么原因