You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中@Binding、@Published与Observable值传递异常排查

问题:SwiftUI嵌套ObservableObject状态更新无法传递到上层视图

我正在开发一款iPhone应用,在使用@Binding、@ObservedObject和@Published属性时遇到了问题。原本认为实现方式正确,但实际运行仍有异常。减少视图嵌套层级后功能恢复正常,以下是复现问题的最小示例:

import SwiftUI

// character
class TopLevelParent: ObservableObject  {
    var id: UUID = UUID()
    @Published var prop: [Child1] = [Child1(), Child1(), Child1()]
}

class TestState: ObservableObject {
    @Published var tlp: TopLevelParent = TopLevelParent()
}

// level
class Child1: ObservableObject, Identifiable {
    var id: UUID = UUID()
    @Published var prop: [Child2] = [Child2(), Child2()]
}

// levelskill
class Child2: ObservableObject, Identifiable {
    var id: UUID = UUID()
    @Published var clicked: Int = 0
    @Published var maxClick: Int = 3
    @Published var prop: [YoungestChild] = [YoungestChild(), YoungestChild(), YoungestChild(), YoungestChild(), YoungestChild()]
}

// skill
class YoungestChild: ObservableObject, Identifiable {
    var id: UUID = UUID()
    @Published var name: String = "name"
    @Published var hasIncreased: Bool = false
}

struct TestView: View {
    @EnvironmentObject var state: TestState
    
    var body: some View {
        VStack {
            NavigationStack {
                List {
                    ForEach(state.tlp.prop, id:\.id) { prop in
                        NavigationLink(destination: TestView2(testclass2: prop)) {
                            VStack {
                                TestView5(testclass2: prop)
                            }
                        }
                    }
                }
            }
        }
    }
}

struct TestView2: View {
    @ObservedObject var testclass2: Child1
    @State private var activeSkillBonus: UUID?
    
    var body: some View {
        VStack (alignment: .leading) {
            let testclass3 = testclass2.prop
            VStack (alignment: .leading) {
                ForEach(testclass3) { index in
                    TestView3(testclass3: index, activeSkillBonuses: $activeSkillBonus)
                }
            }
        }
    }
}

struct TestView3: View {
    @ObservedObject var testclass3: Child2
    @Binding var activeSkillBonuses: UUID?
    var body: some View {
        VStack (alignment: .leading)
        {
            VStack {
                Text("Skill Boosts - \(testclass3.clicked)/\(testclass3.maxClick)")
                Text("\(testclass3.id)")
            }
            .foregroundStyle(Color.white)
            .background(RoundedRectangle(cornerRadius: 20)
                .foregroundStyle(Color.oxblood))
            .onTapGesture{
                withAnimation {
                    if activeSkillBonuses == testclass3.id {
                        activeSkillBonuses = nil
                    } else {
                        activeSkillBonuses = testclass3.id
                    }
                }
            }
            if activeSkillBonuses == testclass3.id  {
                VStack {
                    ForEach(testclass3.prop) { index in
                        TestView4(testclass4: index, testclass3: testclass3)
                    }
                }
            }
        }
    }
}

struct TestView4: View {
    @ObservedObject var testclass4: YoungestChild
    @ObservedObject var testclass3: Child2
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(testclass4.name)
            }
        }
        .foregroundStyle(testclass4.hasIncreased ? Color.red : Color.primary)
        .background(testclass4.hasIncreased ? Color.green : Color.brown)
        .onTapGesture {
            testclass3.objectWillChange.send()
            if testclass4.hasIncreased {
                testclass3.clicked -= 1
            } else
            {
                testclass3.clicked += 1
            }
            testclass4.hasIncreased.toggle()
        }
    }
}

struct TestView5: View {
    @ObservedObject var testclass2: Child1
    var body: some View {
        VStack {
            ForEach(testclass2.prop) {testclass3 in
                Text("Skill Bonus - \(testclass3.clicked)/\(testclass3.maxClick)")
            }
        }
    }
}
    
struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
            .environmentObject(TestState())
    }
}

具体场景:TestView作为顶层视图,在NavigationStack中包含多个TestView5;点击TestView5会打开TestView2,其包含多个TestView3;每个TestView3下的TestView4被点击时会更新Child2的数值,该数值可在TestView3中正常更新,但TestView5始终显示初始值(0/3)。


问题原因

核心问题在于**Child1作为ObservableObject,无法感知其内部@Published数组中Child2对象的属性变化**:

  • @Published对于数组类型,只会在数组本身被替换(比如重新赋值新数组)或数组元素的引用发生变化时,发送objectWillChange通知。
  • 当Child2的clicked属性更新时,只有直接观察该Child2的视图(比如TestView3)会收到刷新通知;而TestView5观察的是Child1,Child1没有收到Child2变化的通知,因此不会触发TestView5的刷新。

解决方案

方案1:让Child1监听Child2的状态变化

修改Child1类,添加对其内部Child2对象的订阅,当Child2状态变化时,主动触发Child1的更新通知:

class Child1: ObservableObject, Identifiable {
    var id: UUID = UUID()
    @Published var prop: [Child2] = [Child2(), Child2()]
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        setupChildSubscriptions()
    }
    
    // 监听prop数组的变化,为每个Child2添加状态订阅
    private func setupChildSubscriptions() {
        $prop
            .sink { [weak self] newChildren in
                self?.cancellables.removeAll()
                newChildren.forEach { child in
                    child.objectWillChange
                        .sink { [weak self] _ in
                            self?.objectWillChange.send()
                        }
                        .store(in: &self!.cancellables)
                }
            }
            .store(in: &cancellables)
    }
}

这样只要Child2的状态发生变化,Child1就会发送更新通知,TestView5就能同步刷新显示最新的clicked值。

方案2:改用值类型(Struct)替代引用类型(Class)

SwiftUI对值类型的状态更新天然友好,当Struct的属性变化时,整个实例会被视为新值,自动触发视图刷新。将所有嵌套数据模型改为Struct:

// 替换原有的Class定义
struct Child1: Identifiable {
    var id: UUID = UUID()
    var prop: [Child2] = [Child2(), Child2()]
}

struct Child2: Identifiable {
    var id: UUID = UUID()
    var clicked: Int = 0
    var maxClick: Int = 3
    var prop: [YoungestChild] = Array(repeating: YoungestChild(), count: 5)
}

struct YoungestChild: Identifiable {
    var id: UUID = UUID()
    var name: String = "name"
    var hasIncreased: Bool = false
}

此时需要注意:修改Struct属性时,需要通过@Binding或者在顶层ObservableObject中对数组进行修改(因为值类型是不可变的,需要生成新实例替换原数组元素)。这种方式能简化状态管理,避免嵌套ObservableObject的通知传递问题。

方案3:在TestView5中直接观察Child2(不推荐)

如果只针对当前场景,也可以修改TestView5,让其直接观察每个Child2对象:

struct TestView5: View {
    @ObservedObject var testclass2: Child1
    var body: some View {
        VStack {
            ForEach(testclass2.prop) { testclass3 in
                // 用ObservedObject包装Child2,触发视图刷新
                TestView5Child(testclass3: testclass3)
            }
        }
    }
}

// 新增子视图观察单个Child2
struct TestView5Child: View {
    @ObservedObject var testclass3: Child2
    var body: some View {
        Text("Skill Bonus - \(testclass3.clicked)/\(testclass3.maxClick)")
    }
}

这种方式虽然能解决问题,但如果层级更深,会导致视图嵌套冗余,不如前两种方案优雅。


内容的提问来源于stack exchange,提问作者phroureo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 10:45:56