SwiftUI删除数组元素后onReceive触发数组越界致命错误
问题分析与解决方案
问题根源
删除模块后触发索引越界,核心原因有两点:
- 视图销毁滞后:直接修改ViewModel数组删除模块时,SwiftUI视图更新存在短暂延迟,已删除模块的子视图(如
CustomTextField)仍持有失效的绑定引用,触发订阅时访问了不存在的数据。 - 冗余绑定操作:
CustomTextField中onReceive里的value = newValue完全多余(TextField已通过双向绑定同步值),该操作会在绑定对象失效时引发非法访问。
修复方案
1. 将删除逻辑封装到ViewModel,保证数据一致性
把模块删除操作移到ViewModel中,避免视图层直接操作数组索引,让SwiftUI更精准地跟踪数据变化:
class ViewModel: ObservableObject { @Published var classModules: [ClassModule] = [/* 原有初始化数据 */] // 添加统一删除方法 func deleteModule(withId id: String) { classModules.removeAll { $0.id == id } } }
在ContentView中调用该方法替代原有的索引删除:
struct ContentView: View { @ObservedObject var viewModel: ViewModel = ViewModel() var body: some View { ForEach($viewModel.classModules) { $classModule in ClassModuleView(classModule: $classModule, remove: { viewModel.deleteModule(withId: classModule.id) }) } } }
2. 修复CustomTextField的订阅逻辑
移除冗余的赋值操作,若需处理文本变更,改用onChange(仅在视图有效时触发,避免访问失效绑定):
struct CustomTextField: View { @Binding var value: String var body: some View { let _ = print("Rendering textfield: \(value)") TextField("", text: $value) .onChange(of: value) { newValue in print("onChange", newValue) // 这里添加自定义文本处理逻辑,比如过滤、格式化 value = newValue.trimmingCharacters(in: .whitespaces) } } }
3. 可选:明确ForEach的标识(增强稳定性)
虽然ClassModule已实现Identifiable,明确指定id可进一步避免视图识别错误:
ForEach($viewModel.classModules, id: \.id) { $classModule in ClassModuleView(classModule: $classModule, remove: { viewModel.deleteModule(withId: classModule.id) }) }
完整修正代码
import SwiftUI import Combine class ViewModel: ObservableObject { @Published var classModules: [ClassModule] = [ ClassModule(name: "Maths", students: [ Student(name: "John"), Student(name: "Emma"), Student(name: "James") ]), ClassModule(name: "Science", students: [ Student(name: "David"), Student(name: "Kerry"), Student(name: "Anna") ]), ClassModule(name: "Art", students: [ Student(name: "Georgina"), Student(name: "Bob"), Student(name: "Fred") ]) ] func deleteModule(withId id: String) { classModules.removeAll { $0.id == id } } } struct ClassModule: Identifiable, Hashable { let id: String = UUID().uuidString var name: String var students: [Student] } struct Student: Identifiable, Hashable { let id: String = UUID().uuidString var name: String } struct ContentView: View { @ObservedObject var viewModel: ViewModel = ViewModel() var body: some View { ForEach($viewModel.classModules, id: \.id) { $classModule in ClassModuleView(classModule: $classModule, remove: { viewModel.deleteModule(withId: classModule.id) }) } } } struct ClassModuleView: View { @Binding var classModule: ClassModule var remove: () -> Void var body: some View { VStack { HStack { Text(classModule.name) .font(.title) Spacer() Button(action: remove, label: { Text("Delete") }) } Group { ForEach($classModule.students) { $student in StudentView(student: $student) } } } } } struct StudentView: View { @Binding var student: Student var body: some View { CustomTextField(value: $student.name) } } struct CustomTextField: View { @Binding var value: String var body: some View { let _ = print("Rendering textfield: \(value)") TextField("", text: $value) .onChange(of: value) { newValue in print("onChange", newValue) // 自定义文本处理示例:去除首尾空格 value = newValue.trimmingCharacters(in: .whitespaces) } } } #Preview { ContentView() }
内容的提问来源于stack exchange,提问作者jamesrogers93
相关产品推荐
相关产品推荐

