SwiftUI多层嵌套ObservableObject模型视图不更新,求正确组织方式
问题场景
当前使用多层嵌套的ObservableObject类作为数据模型:
class TaskDto : Identifiable, Equatable, ObservableObject { var id: String @Published var fileName:String @Published var name: String // 其他属性 @Published var parameters : JobParametersDto }
class JobParametersDto : Identifiable, Equatable, ObservableObject { var id: String @Published var blobs: [BlobRequestDto] @Published var commands:[CommandDto] @Published var mainTask: String }
对应视图BlobView通过@ObservedObject绑定顶层TaskDto,但修改嵌套层级的属性(比如blobs数组元素的Unzip开关)时,视图无法实时刷新,必须点击其他字段才会更新状态:
struct BlobView: View { @ObservedObject var taskDto: TaskDto static let blobNames = [ "Product", "Id", "Name", "Version", "Limit", "Unzip"] let columns = Array(repeating: GridItem(.flexible(minimum: 100)), count: blobNames.count) @State private var scrollViewContentSize: CGSize = .zero var body: some View { GridView(title: "Blob Names") { LazyVGrid(columns: columns) { ForEach(Self.blobNames, id: \.self) { blobName in Text("\(blobName)").bold() } ForEach($taskDto.parameters.blobs) { blob in if !blob.Removed.wrappedValue { TextField("", text: blob.BlobId) TextField("", text: blob.BlobName) TextField("", text: blob.BlobVersion) Toggle("", isOn: blob.Unzip) } } } } } }
问题原因
SwiftUI的@Published仅能监听直接属性的变化:当嵌套的ObservableObject内部属性更新时,顶层的@Published var parameters: JobParametersDto只会在parameters对象本身被替换时发送通知,无法感知其内部blobs数组或数组元素的属性变化。同理,若BlobRequestDto也是ObservableObject,其内部属性更新也不会触发上层视图刷新。
解决方案
方案1:将嵌套模型改为值类型(Struct)
把所有嵌套的ObservableObject类改为struct,利用Swift值类型的特性,修改嵌套属性时会自动触发顶层@Published的变化通知:
// 改为struct,无需实现ObservableObject struct TaskDto : Identifiable, Equatable { var id: String var fileName:String var name: String // 其他属性 var parameters : JobParametersDto } struct JobParametersDto : Identifiable, Equatable { var id: String var blobs: [BlobRequestDto] var commands:[CommandDto] var mainTask: String } struct BlobRequestDto: Identifiable, Equatable { var id: String var Removed: Bool var BlobId: String var BlobName: String var BlobVersion: String var Unzip: Bool }
视图中使用@Binding或绑定顶层ObservableObject(若顶层仍需用类),修改嵌套属性时,值类型的变更会自动触发视图刷新。
方案2:手动转发嵌套对象的订阅(保留引用类型)
若必须使用引用类型,需在顶层模型中手动监听嵌套ObservableObject的变化,并转发通知:
import Combine class TaskDto : Identifiable, Equatable, ObservableObject { var id: String @Published var fileName:String @Published var name: String @Published var parameters : JobParametersDto { didSet { // 移除旧对象的订阅 oldParamCancellable?.cancel() // 订阅新对象的变化,触发自身的objectWillChange oldParamCancellable = parameters.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() } } } private var oldParamCancellable: AnyCancellable? // 初始化时订阅parameters的变化 init(id: String, fileName: String, name: String, parameters: JobParametersDto) { self.id = id self.fileName = fileName self.name = name self.parameters = parameters oldParamCancellable = parameters.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() } } // 实现Equatable协议(略) static func == (lhs: TaskDto, rhs: TaskDto) -> Bool { return lhs.id == rhs.id } }
同理,JobParametersDto需监听blobs数组中每个BlobRequestDto的变化(若BlobRequestDto是ObservableObject):
class JobParametersDto : Identifiable, Equatable, ObservableObject { var id: String @Published var blobs: [BlobRequestDto] { didSet { updateBlobSubscriptions() } } @Published var commands:[CommandDto] @Published var mainTask: String private var blobCancellables: [AnyCancellable] = [] private func updateBlobSubscriptions() { blobCancellables.forEach { $0.cancel() } blobCancellables = blobs.map { blob in blob.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() } } } // 初始化时调用订阅方法 init(id: String, blobs: [BlobRequestDto], commands: [CommandDto], mainTask: String) { self.id = id self.blobs = blobs self.commands = commands self.mainTask = mainTask updateBlobSubscriptions() } // 实现Equatable协议(略) static func == (lhs: JobParametersDto, rhs: JobParametersDto) -> Bool { return lhs.id == rhs.id } }
这种方式通过手动转发嵌套对象的objectWillChange事件,让顶层模型感知内部属性变化,从而触发视图刷新。
方案3:拆分视图,直接绑定嵌套对象
将嵌套层级的视图拆分,让子视图直接绑定对应的嵌套ObservableObject,而非通过顶层模型传递:
// 子视图直接绑定JobParametersDto struct BlobView: View { @ObservedObject var parameters: JobParametersDto static let blobNames = [ "Product", "Id", "Name", "Version", "Limit", "Unzip"] let columns = Array(repeating: GridItem(.flexible(minimum: 100)), count: blobNames.count) var body: some View { GridView(title: "Blob Names") { LazyVGrid(columns: columns) { ForEach(Self.blobNames, id: \.self) { blobName in Text("\(blobName)").bold() } ForEach($parameters.blobs) { blob in if !blob.Removed.wrappedValue { TextField("", text: blob.BlobId) TextField("", text: blob.BlobName) TextField("", text: blob.BlobVersion) Toggle("", isOn: blob.Unzip) } } } } } }
在父视图中传递嵌套对象:
struct ParentView: View { @ObservedObject var taskDto: TaskDto var body: some View { BlobView(parameters: taskDto.parameters) } }
子视图可直接监听JobParametersDto的变化,包括其内部blobs数组的更新。
推荐方案
优先选择方案1(值类型Struct),SwiftUI对值类型的支持更自然,无需手动管理订阅,代码简洁且不易出错。仅在必须使用引用类型的场景(如需要共享引用、跨对象修改),再考虑方案2或方案3。
内容的提问来源于stack exchange,提问作者user2555515

