You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 13:37:02