嵌套 ObservedObject 中的更改不会更新 UI

Posted

技术标签:

【中文标题】嵌套 ObservedObject 中的更改不会更新 UI【英文标题】:Changes in nested ObservedObject do not updated the UI 【发布时间】:2021-07-15 20:40:33 【问题描述】:

当我有一个嵌套的 ObservedObject 时,嵌套对象的已发布属性中的更改不会更新 UI,直到父对象发生某些事情。这是我的代码中的功能、错误(在 SwiftUI 中)还是错误?

这是一个简化的例子。单击父级的开/关按钮会立即更新 UI,但单击子级的开/关按钮在父级更新之前不会更新。

我正在运行 Xcode 12.5.1。

import SwiftUI

class NestedObject: NSObject, ObservableObject 
    @Published var flag = false

class StateObject: NSObject, ObservableObject 
    @Published var flag = false
    @Published var nestedState = NestedObject()


struct ContentView: View 
    @ObservedObject var state = StateObject()
    var body: some View 
        VStack 
            HStack 
                Text("Parent:")
                Button(action: 
                    state.flag.toggle()
                , label: 
                    Text(state.flag ? "On" : "Off")
                )
            
            HStack 
                Text("Child:")
                Button(action: 
                    state.nestedState.flag.toggle()
                , label: 
                    Text(state.nestedState.flag ? "On" : "Off")
                )
            
        
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

【问题讨论】:

【参考方案1】:

@ObservedObject@StateObjectObservableObject 更新时更新视图。当 @Published 属性更改或直接调用 objectWillChange.send() 时会发生这种情况。

因此,“正常”(也是最简单的)方法是使用值类型,例如struct,用于 @Published 属性。

struct NestedObject 
   var flag = false

这样做的原因是,整个NestedObject 在其属性被修改时会发生变化,因为struct 是一个值类型。相反,引用类型class 在其属性被修改时不会改变(即引用保持不变)。


但是,有时你可能需要它是一个引用类型的对象,因为它可能有自己的生命周期,等等......

在这种情况下,您绝对可以只调用state.objectWillChange.send(),但这仅在视图启动更改时才有效,而不是在嵌套对象启动更改时。在我看来,这里最好的通用方法是使用嵌套的内部视图,它有自己的@ObservedObject 来观察内部对象的变化:

struct ContentView: View 

    private struct InnerView: View 
        @ObservedObject var model: NestedObject
        var body: some View 
            Text("Child:")
            Button(action: 
                model.flag.toggle()
            , label: 
                Text(model.flag ? "On" : "Off")
            )
        
    

    @StateObject var state = OuterObject() // see comments 1, 2 below

    var body: some View 
        VStack 
            HStack 
                Text("Parent:")
                Button(action: 
                    state.flag.toggle()
                , label: 
                    Text(state.flag ? "On" : "Off")
                )
            
            HStack 
                InnerView(model: state.nestedObject)
            
        
    


1。你不应该调用你的类StateObject,因为它与SwiftUI 的StateObject 属性包装器冲突。我将其重命名为OuterObject

2。此外,如果在视图中实例化对象,则应使用@StateObject 而不是@ObservedObject

【讨论】:

【参考方案2】:

它按预期工作:ObservableObject 仅发送有关 @Published 属性更改的通知,但不传播嵌套 ObservableObjects 的更改通知。

我会遵循advice from New Dev:尽可能使用结构或使用单独的视图来订阅嵌套对象。

如果您确实需要嵌套的 ObservableObjects,您可以使用 Combine 将 objectWillChange 事件从嵌套对象传播到外部对象:

import Combine
import SwiftUI

class InnerObject: ObservableObject 
    @Published var flag = false


class OuterObject: ObservableObject 
    @Published var flag = false
    var innerObject = InnerObject() 
        didSet 
            subscribeToInnerObject()
        
    

    init() 
        subscribeToInnerObject()
    

    private var innerObjectSubscription: AnyCancellable?

    private func subscribeToInnerObject() 
        // subscribe to the inner object and propagate the objectWillChange notification if it changes
        innerObjectSubscription = innerObject.objectWillChange.sink(receiveValue: objectWillChange.send)
    


struct ContentView: View 
    @ObservedObject var state = OuterObject()
    var body: some View 
        VStack 
            Toggle("Parent \(state.flag ? "On" : "Off")", isOn: $state.flag)
            Toggle("Child \(state.innerObject.flag ? "On" : "Off")", isOn: $state.innerObject.flag)
        
        .padding()
    

【讨论】:

【参考方案3】:

感谢您的澄清。对我来说最有价值的一点是,这种行为不是(SwiftUI 的)错误,而是一种设计行为。

SwiftUI(更准确地说是Combine)只能看到值的变化,因此它可以看到@Published struct 实例的属性值变化,但不能看到@Published 类实例的变化。

因此,答案是“如果你想根据那些嵌套对象的属性值的变化来更新 UI,就使用嵌套对象的结构实例。如果你必须使用类实例,使用另一种机制来显式通知变化”。

这里是使用 struct 代替 class 的 NestedObject 的修改代码。

import SwiftUI

struct NestedObject 
    var flag = false

class OuterObject: NSObject, ObservableObject 
    @Published var flag = false
    @Published var nestedState = NestedObject()


struct ContentView: View 
    @ObservedObject var state = OuterObject()
    var body: some View 
        VStack 
            HStack 
                Text("Parent:")
                Button(action: 
                    state.flag.toggle()
                , label: 
                    Text(state.flag ? "On" : "Off")
                )
            
            HStack 
                Text("Child:")
                Button(action: 
                    state.nestedState.flag.toggle()
                , label: 
                    Text(state.nestedState.flag ? "On" : "Off")
                )
            
        
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

【讨论】:

【参考方案4】:

试试这个,对我来说在 ios-15、catalyst 15、macos 12 上运行良好,使用 xcode 13:

        HStack 
            Text("Child:")
            Button(action: 
                state.objectWillChange.send()  // <--- here
                state.nestedState.flag.toggle()
            , label: 
                Text(state.nestedState.flag ? "On" : "Off")
            )
        

【讨论】:

以上是关于嵌套 ObservedObject 中的更改不会更新 UI的主要内容,如果未能解决你的问题,请参考以下文章

ObservedObject 更改时的通知

为啥我的@ObservedObject 没有在我的视图中执行更改?

当 ObservedObject 更改时,在 SwiftUI 视图中未调用 onReceive

SmartEdit:除非刷新页面,否则嵌套 CMS 组件中的更改不会反映

@ObservedObject 更改后 SwiftUI 视图未更新

ObservedObject 不会注意到 ObservableObject Array 的变化