I have a UIKit app where I've adopted SwiftData and I'm struggling with a crash coming in from some of my users. I'm not able to reproduce it myself and as it only happens to a small fraction of my user base, it seems like a race condition of some sort.
This is the assertion message:
SwiftData/DefaultStore.swift:453: Fatal error: API Contract Violation: Editors must register their identifiers before invoking operations on this store SwiftData.DefaultStore: 00CF060A-291A-4E79-BEC3-E6A6B20F345E did not.
(ID is unique per crash)
This is the ModelActor that crashes:
@available(iOS 17, *)
@ModelActor
actor ConsumptionDatabaseStorage: ConsumptionSessionStorage {
struct Error: LocalizedError {
var errorDescription: String?
}
private let sortDescriptor = [SortDescriptor(\SDConsumptionSession.startTimeUtc, order: .reverse)]
static func createStorage(userId: String) throws -> ConsumptionDatabaseStorage {
guard let appGroupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: UserDefaults.defaultAppGroupIdentifier) else {
throw Error(errorDescription: "Invalid app group container ID")
}
func createModelContainer(databaseUrl: URL) throws -> ModelContainer {
return try ModelContainer(for: SDConsumptionSession.self, SDPriceSegment.self, configurations: ModelConfiguration(url: databaseUrl))
}
let databaseUrl = appGroupContainer.appendingPathComponent("\(userId).sqlite")
do {
return self.init(modelContainer: try createModelContainer(databaseUrl: databaseUrl))
} catch {
// Creating the model storage failed. Remove the database file and try again.
try? FileManager.default.removeItem(at: databaseUrl)
return self.init(modelContainer: try createModelContainer(databaseUrl: databaseUrl))
}
}
func isStorageEmpty() async -> Bool {
(try? self.modelContext.fetchCount(FetchDescriptor<SDConsumptionSession>())) ?? 0 == 0 // <-- Crash here!
}
func sessionsIn(interval: DateInterval) async throws -> [ConsumptionSession] {
let fetchDescriptor = FetchDescriptor(predicate: #Predicate<SDConsumptionSession> { sdSession in
if let startDate = sdSession.startTimeUtc {
return interval.start <= startDate && interval.end > startDate
} else {
return false
}
}, sortBy: self.sortDescriptor)
let consumptionSessions = try self.modelContext.fetch(fetchDescriptor) // <-- Crash here!
return consumptionSessions.map { ConsumptionSession(swiftDataSession: $0) }
}
func updateSessions(sessions: [ConsumptionSession]) async throws {
if #unavailable(iOS 18) {
// Price segments are duplicated if re-inserted so unfortunately we have to delete and reinsert sessions.
// On iOS 18, this is enforced by the #Unique macro on SDPriceSegment.
let sessionIds = Set(sessions.map(\.id))
try self.modelContext.delete(model: SDConsumptionSession.self, where: #Predicate<SDConsumptionSession> {
sessionIds.contains($0.id)
})
}
for session in sessions {
self.modelContext.insert(SDConsumptionSession(consumptionSession: session))
}
if self.modelContext.hasChanges {
try self.modelContext.save()
}
}
func deleteAllSessions() async {
if #available(iOS 18, *) {
try? self.modelContainer.erase()
} else {
self.modelContainer.deleteAllData()
}
}
}
The actor conforms to this protocol:
protocol ConsumptionSessionStorage {
func isStorageEmpty() async -> Bool
func hasCreditCardSessions() async -> Bool
func sessionsIn(interval: DateInterval) async throws -> [ConsumptionSession]
func updateSessions(sessions: [ConsumptionSession]) async throws
func deleteAllSessions() async
}
The crash is coming in from line 30 and 41, in other words, when trying to fetch data from the database. There doesn't seem to be any common trait for the crashes. They occur across iOS versions and device types.
Any idea what might cause this?
Update: I managed to reproduce the crash and solve it (I think). I had a scenario where the ModelActor in some cases would leak and stay transient after the user logged out. If another user logged in afterwards and another instance of the ModelActor would be created, this crash would occur. After I fixed the leak, the app seemingly no longer crashes.
It's still a bit of a mystery to me, why it would crash though. The ModelActors are initialized with their own user-specific database, so they are completely distinct. My theory is that the leaked database could reference some resources that no longer existed after the user logged out and that caused the crash.