编辑子视图后UI未更新的SwiftUI技术问题
我的数据模型是包含对象数组的类:
class InnerDataItem : Identifiable, ObservableObject { var id = UUID() @Published var innerInfo = "" init(innerInfo: String = "") { self.innerInfo = innerInfo } } class MyDataModel : Identifiable, ObservableObject { let id: UUID = UUID() @Published var info = "My String array" @Published var innerData: [InnerDataItem] = [] init(info: String = "My String array", innerData: [InnerDataItem]) { self.info = info self.innerData = innerData } }
定义了该模型的全局实例:
let modelData = MyDataModel( info: "Data model object", innerData: [ InnerDataItem(innerInfo: "Inner info One"), InnerDataItem(innerInfo: "Inner info Two"), InnerDataItem(innerInfo: "Inner info Three"), ])
ContentView通过App传入模型,展示innerData列表:
@main struct TestApp: App { var body: some Scene { WindowGroup { ContentView(dataModel: modelData) } } } struct ContentView: View { @ObservedObject var dataModel: MyDataModel var body: some View { NavigationView { List { ForEach(dataModel.innerData) { innerItem in NavigationLink { InnerDataItemEditView(data: innerItem) } label: { Text(innerItem.innerInfo) } }.onMove(perform: { indices, newOffset in dataModel.move(fromOffsets: indices, toOffset: newOffset) }) } .navigationBarItems(trailing: EditButton()) } } }
InnerDataItemEditView用TextField修改子项的值:
struct InnerDataItemEditView: View { @ObservedObject var data: InnerDataItem var body: some View { TextField("Info: ", text: $data.innerInfo) } }
问题:编辑InnerDataItem的值时,数据已更新,但ContentView的List不会同步刷新;只有用Edit按钮重排序后,修改的值才会显示。尝试传递Binding到InnerDataItemEditView但无法编译,请问UI无法正常更新的原因是什么?
核心原因
ContentView的List依赖dataModel.innerData的@Published通知触发刷新,但innerData是ObservableObject对象数组。当数组内单个InnerDataItem的内部@Published属性更新时,MyDataModel的innerData数组本身的引用没有变化,因此dataModel不会发送objectWillChange通知,ContentView也就不会刷新。而重排序操作修改了数组的元素顺序,触发了innerData的@Published通知,才会让List刷新并显示最新值。
解决方案
有两种可行的修复方式:
方式一:将InnerDataItem改为值类型(struct)
把InnerDataItem从class改成struct,去掉ObservableObject协议(值类型的属性变化会直接触发数组的变化,进而触发MyDataModel的@Published通知):
struct InnerDataItem : Identifiable { var id = UUID() var innerInfo = "" init(innerInfo: String = "") { self.innerInfo = innerInfo } }
同时修改InnerDataItemEditView为接收Binding:
struct InnerDataItemEditView: View { @Binding var data: InnerDataItem var body: some View { TextField("Info: ", text: $data.innerInfo) } }
在ContentView的ForEach中传递Binding:
ForEach($dataModel.innerData) { $innerItem in NavigationLink { InnerDataItemEditView(data: $innerItem) } label: { Text(innerItem.innerInfo) } }
这种方式更符合SwiftUI的设计理念,值类型的状态管理更简单直观。
方式二:监听子项变化并通知父模型
如果必须保留InnerDataItem为类类型,需要在MyDataModel中监听每个InnerDataItem的objectWillChange事件,当子项变化时手动触发自身的objectWillChange通知:
class MyDataModel : Identifiable, ObservableObject { let id: UUID = UUID() @Published var info = "My String array" @Published var innerData: [InnerDataItem] = [] { willSet { // 移除旧子项的监听 innerData.forEach { stopListening(to: $0) } // 添加新子项的监听 newValue.forEach { startListening(to: $0) } } } private var cancellables: [UUID: AnyCancellable] = [:] init(info: String = "My String array", innerData: [InnerDataItem]) { self.info = info self.innerData = innerData // 初始化时添加监听 innerData.forEach { startListening(to: $0) } } private func startListening(to item: InnerDataItem) { let cancellable = item.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() } cancellables[item.id] = cancellable } private func stopListening(to item: InnerDataItem) { cancellables.removeValue(forKey: item.id) } // 实现move方法 func move(fromOffsets indices: IndexSet, toOffset newOffset: Int) { innerData.move(fromOffsets: indices, toOffset: newOffset) } }
这样,当任何一个InnerDataItem的属性变化时,MyDataModel会主动发送通知,ContentView的List就会同步刷新了。
内容的提问来源于stack exchange,提问作者Per Lindgren

