SwiftData访问结构体属性时崩溃的原因及解决方法
SwiftData中关联Codable枚举类型导致崩溃及修复方案
模型定义
我在Swift iOS应用中定义了以下模型:
import Foundation import SwiftData @Model class User: Identifiable { let id: UUID let name: String var location: NamedLocation? var friend: Bool = false init(id: UUID = UUID(), name: String) { self.id = id self.name = name } } struct NamedLocation: Codable { let name: String? let loc: Location init(name: String?, location: Location) { self.name = name self.loc = location } } enum Location: Codable { case address(String) case coordinates(Double, Double) }
正常测试场景
填充测试数据查看UI效果时,一切正常:
import SwiftUI import SwiftData struct HomeView: View { @Environment(\.modelContext) private var modelContext @Query private var items: [User] var body: some View { // 省略其他视图代码 VStack(alignment: .leading, spacing: 16.0) { Text("your nearby friends") List { ForEach(items) { item in HStack { VStack { Text(item.name) if let location = item.location { Text(location.name ?? "unknown location") } } } } } .listStyle(.plain) .scrollContentBackground(.hidden) Spacer() } } } #Preview { let config = ModelConfiguration(isStoredInMemoryOnly: true, allowsSave: true) let container = try! ModelContainer(for: Schema([User.self]), configurations: config) for i in 1..<10 { var user = User(name: "Example User \(i)") container.mainContext.insert(user) } return HomeView().modelContainer(container) }
崩溃场景
但一旦为用户添加location属性,预览和模拟器就会崩溃:
for i in 1..<10 { var user = User(name: "Example User \(i)") container.mainContext.insert(user) // 参考其他SO回答尝试先保存再修改,无效 try! container.mainContext.save() user.location = NamedLocation(name: "hurk", location: .coordinates(40.72, -75.49)) try! container.mainContext.save() }
崩溃类型为EXC_BREAKPOINT,发生在User.location.getter,崩溃日志如下:
Exception Type: EXC_BREAKPOINT (SIGTRAP) Exception Codes: 0x0000000000000001, 0x00000001c4352d5c Termination Reason: SIGNAL 5 Trace/BPT trap: 5 Terminating Process: exc handler [82555] Triggered by Thread: 0 Thread 0 Crashed:: Dispatch queue: com.apple.main-thread 0 SwiftData 0x1c4352d5c 0x1c42d1000 + 531804 1 SwiftData 0x1c435597c 0x1c42d1000 + 543100 2 SwiftData 0x1c4316014 0x1c42d1000 + 282644 3 rdvz 0x100357830 User.location.getter + 332 (@__swiftmacro_4rdvz4UserC8location18_PersistedPropertyfMa_.swift:10) 4 HomeView.1.preview-thunk.dylib 0x101592f28 closure #1 in closure #1 in closure #1 in closure #1 in closure #1 in closure #2 in closure #1 in HomeView.__preview__body.getter + 476 (HomeView.swift:73) 5 SwiftUI 0x1c49e38b8 0x1c4371000 + 6760632 6 HomeView.1.preview-thunk.dylib 0x101592b7c closure #1 in closure #1 in closure #1 in closure #1 in closure #2 in closure #1 in HomeView.__preview__body.getter + 144 (HomeView.swift:68) 7 SwiftUI 0x1c52e0840 0x1c4371000 + 16185408 8 HomeView.1.preview-thunk.dylib 0x1015928cc closure #1 in closure #1 in closure #1 in closure #2 in closure #1 in HomeView.__preview__body.getter + 148 (HomeView.swift:67) 9 SwiftUI 0x1c532f9a0 0x1c4371000 + 16509344 10 SwiftUI 0x1c53379e4 0x1c4371000 + 16542180 11 SwiftUI 0x1c4cfd750 0x1c4371000 + 10012496 12 SwiftUI 0x1c5337a00 0x1c4371000 + 16542208 13 libswiftCore.dylib 0x192adba04 withUnsafePointer<A, B>(to:_:) + 20 14 libswiftCore.dylib 0x192c862ac withUnsafeMutablePointer<A, B>(to:_:) + 12 15 SwiftUI 0x1c532f0f0 0x1c4371000 + 16507120 16 SwiftUI 0x1c5330e24 0x1c4371000 + 16514596 17 SwiftUI 0x1c5330838 0x1c4371000 + 16513080 18 SwiftUI 0x1c533575c 0x1c4371000 + 16533340 19 SwiftUI 0x1c52f7950 0x1c4371000 + 16279888 20 SwiftUI 0x1c497bc10 0x1c4371000 + 6335504 21 SwiftUI 0x1c5374abc 0x1c4371000 + 16792252
补充发现
- 编辑1:如果从
NamedLocation中移除loc成员,或将其设为可选并赋值nil,预览就不再崩溃。 - 编辑2:尝试手动实现
Location的Codable,但编码和解码的数据不匹配:
enum Location: Codable { case address(String) case coordinates(Double, Double) enum CodingKeys: String, CodingKey { case kind case address case latitude case longitude } init(from decoder: Decoder) throws { let container = try! decoder.container(keyedBy: CodingKeys.self) // 断言失败;SwiftData返回的容器包含"address"和"coordinates"键 let kind = try! container.decode(String.self, forKey: .kind) switch kind { case "address": let address = try! container.decode(String.self, forKey: .address) self = .address(address) case "coordinates": let latitude = try! container.decode(Double.self, forKey: .latitude) let longitude = try! container.decode(Double.self, forKey: .longitude) self = .coordinates(latitude, longitude) default: throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Invalid location type") } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .address(let address): try! container.encode("address", forKey: .kind) try! container.encode(address, forKey: .address) case .coordinates(let latitude, let longitude): try! container.encode("coordinates", forKey: .kind) try! container.encode(latitude, forKey: .latitude) try! container.encode(longitude, forKey: .longitude) } } }
问题原因
SwiftData对带关联值的枚举的Codable支持有特殊逻辑:默认自动生成的Codable实现会将枚举编码为单值容器(比如.address("Beijing")编码为"Beijing",.coordinates(40.72, -75.49)编码为[40.72, -75.49]),而非手动实现中期望的带kind字段的键值结构。当枚举作为非可选属性嵌套在Codable结构体中被SwiftData模型引用时,解码过程中格式不匹配导致解析失败,最终触发location属性getter的崩溃。
修复方案
方案1:调整枚举Codable实现适配SwiftData格式
修改Location的Codable逻辑,匹配SwiftData的默认编码方式:
enum Location: Codable { case address(String) case coordinates(Double, Double) init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() // 优先解析地址类型 if let address = try? container.decode(String.self) { self = .address(address) return } // 解析坐标类型(需是长度为2的数组) if let coords = try? container.decode([Double].self), coords.count == 2 { self = .coordinates(coords[0], coords[1]) return } throw DecodingError.dataCorruptedError(in: container, debugDescription: "无效的Location格式") } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .address(let address): try container.encode(address) case .coordinates(let lat, let lon): try container.encode([lat, lon]) } } }
方案2:将枚举转为SwiftData模型类
如果后续需要对Location做查询等操作,可将其改为@Model类:
@Model class Location: Identifiable { let id: UUID var type: LocationType var address: String? var latitude: Double? var longitude: Double? enum LocationType: String, Codable { case address, coordinates } init(address: String) { self.id = UUID() self.type = .address self.address = address } init(coordinates: (Double, Double)) { self.id = UUID() self.type = .coordinates self.latitude = coordinates.0 self.longitude = coordinates.1 } } // 调整NamedLocation结构(或直接将属性合并到User类) struct NamedLocation: Codable { let name: String? let loc: Location }
方案3:使用外部存储标记属性
在User的location属性上添加@Attribute(.externalStorage),让SwiftData以二进制形式存储整个Codable结构体,跳过枚举解析逻辑:
@Model class User: Identifiable { let id: UUID let name: String @Attribute(.externalStorage) var location: NamedLocation? var friend: Bool = false init(id: UUID = UUID(), name: String) { self.id = id self.name = name } }
验证修改
修改后,预览代码可正常运行:
#Preview { let config = ModelConfiguration(isStoredInMemoryOnly: true, allowsSave: true) let container = try! ModelContainer(for: Schema([User.self]), configurations: config) for i in 1..<10 { var user = User(name: "Example User \(i)") user.location = NamedLocation(name: "hurk", location: .coordinates(40.72, -75.49)) container.mainContext.insert(user) } try! container.mainContext.save() return HomeView().modelContainer(container) }
内容的提问来源于stack exchange,提问作者laptou
相关产品推荐
相关产品推荐

