I've defined the following models in a Swift iOS application:
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)
}
When I try to populate with test data to see how my user interface looks, it works fine:
import SwiftUI
import SwiftData
struct HomeView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [User]
var body: some View {
// other views omitted for brevity
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)
}
But as soon as I add locations to the users, then my preview starts crashing (and it also crashes when I test in the simulator).
for i in 1..<10 {
var user = User(name: "Example User \(i)")
container.mainContext.insert(user)
// I looked at other SO answers, and they suggested the issue comes from
// not saving the model before trying to modify its properties. No dice.
try! container.mainContext.save()
user.location = NamedLocation(name: "hurk", location: .coordinates(40.72, -75.49))
try! container.mainContext.save()
}
The crash is EXC_BREAKPOINT
on User.location.getter
, and the crash logs
don't provide much detail.
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
Why is this crash happening, and how do I fix it?
Edit: If I remove the member loc
from NamedLocation
, or I make it optional and nil
, the preview stops crashing. Why? And how do I make it work?
Edit 2: I found out that the issue is related to how Location
is persisted. I thought that its implementation of Codable
was wrong, so I tried providing a manual one, but it does not work because the data passed to the decoding method does not match the data produced by the encoding method:
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)
// this assertion fails; SwiftData gives a container with keys "address" and "coordinates" for some reason
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)
}
}
}
It looks like SwiftData still can't handle enums with associated values properly. So you need to store the information in another way. One way is to add the associated values as optional properties to the main type.
Below is one such solution where I made them private and used the enum as a computed property
struct NamedLocation: Codable {
let name: String?
private let address: String?
private let longitude: Double?
private let latitude: Double?
var location: Location? {
if let address {
return Location.address(address)
} else if let longitude, let latitude {
return Location.coordinates(longitude, latitude)
} else {
return nil
}
}
init(name: String?, location: Location) {
self.name = name
switch location {
case .address(let string):
address = string
latitude = nil
longitude = nil
case .coordinates(let long, let lat):
longitude = long
latitude = lat
address = nil
}
}
}