SwiftData删除Book后返回列表崩溃问题求助
SwiftData删除数据后返回列表页崩溃问题
崩溃现象
在SwiftUI项目中使用SwiftData管理数据,从BooksListView通过NavigationLink跳转至BookCardDetailsView查看Book详情,在详情页通过Menu的Delete按钮执行modelContext.delete(book)后调用presentationMode.wrappedValue.dismiss()返回列表页时触发崩溃,报错信息:
Could not cast value of type 'Swift.Optional' (0x1e748b1f8) to 'Swift.String' (0x1e7483e68)
该崩溃仅在真机上稳定出现,模拟器偶发,崩溃堆栈关联到Book模型的name属性getter方法。
相关代码
列表页代码
struct BooksListView: View { @Query var books: [Book] @State private var searchedBook: String = "" var newBooks: [Book] { if searchedBook.isEmpty { return books } else { return books.filter {$0.name.contains(searchedBook)} } } var body: some View { ZStack { Color("customBackground").ignoresSafeArea() ScrollView(showsIndicators: false) { ForEach(newBooks) { book in NavigationLink { BookCardDetailsView(book: book) } label: { BookCardView(book: book) } } } .searchable(text: $searchedBook, placement: .navigationBarDrawer(displayMode: .always)) .navigationTitle("Books") } } }
详情页删除逻辑代码
Menu { Button { modelContext.delete(book) presentationMode.wrappedValue.dismiss() } label: { Label("Delete", systemImage: "trash") } } label: { Image(systemName: "ellipsis") .foregroundStyle(Color(.blue)) .padding(6) }
Book模型代码
@Model final class Book: Identifiable { var id = UUID().uuidString var name: String var category: Category? var author: Author? var printDate: String var printLenght: Int var language: String var currentPage: Int = 0 var isbn: String? @Attribute(.externalStorage) var imageData: Data? var isFavorite: Bool = false var isBookmark: Bool = false @Relationship(deleteRule: .nullify, inverse: \Category.books) @Relationship(deleteRule: .nullify, inverse: \Author.books) init(name: String, printDate: String, printLenght: Int, language: String, imageData: Data?) { self.name = name self.printDate = printDate self.printLenght = printLenght self.language = language self.imageData = imageData } }
崩溃关联的name属性代码块
{ @storageRestrictions(accesses: _$backingData, initializes: _name) init(initialValue) { _$backingData.setValue(forKey: \.name, to: initialValue) _name = _SwiftDataNoType() } get { _$observationRegistrar.access(self, keyPath: \.name) return self.getValue(forKey: \.name) } set { _$observationRegistrar.withMutation(of: self, keyPath: \.name) { self.setValue(forKey: \.name, to: newValue) } } }
原因分析
- 对象生命周期冲突:删除Book对象后,SwiftData会将该对象标记为已删除,底层存储的属性值会被置为
nil,但模型中name定义为非可选String。此时立即返回列表页,@Query还未完成数据源同步,newBooks数组中仍包含已删除对象的引用,SwiftUI刷新视图时访问book.name,触发可选值转非可选的类型转换崩溃。 - 模型定义存在瑕疵:
- 自定义
id为字符串类型,SwiftData默认使用内部ID管理对象,自定义字符串ID可能导致对象追踪异常; - 两个
@Relationship注解写在同一位置,未分别绑定到category和author属性,可能破坏SwiftData的关系映射逻辑。
- 自定义
- 视图刷新时机不匹配:删除后立即调用
dismiss(),SwiftData的上下文变更还未完成持久化或通知视图更新,列表页视图仍尝试访问已失效的对象属性。
解决方案
方案1:延迟返回,确保上下文同步
删除后先提交上下文变更,再延迟执行返回操作,给@Query足够时间更新数据源:
Button { modelContext.delete(book) // 强制提交上下文变更 do { try modelContext.save() } catch { print("Save error: \(error)") } // 延迟返回,等待列表页数据源更新 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { presentationMode.wrappedValue.dismiss() } } label: { Label("Delete", systemImage: "trash") }
方案2:修正Book模型定义
- 将自定义ID改为SwiftData推荐的UUID类型并标记为唯一;
- 修正
@Relationship的绑定位置,分别关联到对应属性:
@Model final class Book: Identifiable { @Attribute(.unique) var id: UUID = UUID() var name: String // 分别为每个关联属性添加Relationship注解 @Relationship(deleteRule: .nullify, inverse: \Category.books) var category: Category? @Relationship(deleteRule: .nullify, inverse: \Author.books) var author: Author? var printDate: String var printLenght: Int var language: String var currentPage: Int = 0 var isbn: String? @Attribute(.externalStorage) var imageData: Data? var isFavorite: Bool = false var isBookmark: Bool = false init(id: UUID = UUID(), name: String, printDate: String, printLenght: Int, language: String, imageData: Data?) { self.id = id self.name = name self.printDate = printDate self.printLenght = printLenght self.language = language self.imageData = imageData } }
方案3:使用NavigationStack管理导航(iOS 16+)
改用NavigationStack的path机制管理导航栈,避免已删除对象的引用残留:
struct BooksListView: View { @Query var books: [Book] @State private var searchedBook: String = "" @State private var navigationPath = NavigationPath() var newBooks: [Book] { searchedBook.isEmpty ? books : books.filter {$0.name.contains(searchedBook)} } var body: some View { NavigationStack(path: $navigationPath) { ZStack { Color("customBackground").ignoresSafeArea() ScrollView(showsIndicators: false) { ForEach(newBooks) { book in Button(action: { navigationPath.append(book) }) { BookCardView(book: book) } } } .searchable(text: $searchedBook, placement: .navigationBarDrawer(displayMode: .always)) .navigationTitle("Books") } .navigationDestination(for: Book.self) { book in BookCardDetailsView(book: book) } } } }
删除后可直接通过navigationPath.removeLast()返回,避免无效对象引用。
内容的提问来源于stack exchange,提问作者madDogAttackToSwiftUI
相关产品推荐
相关产品推荐

