I'm using Swift Data for an app that requires iOS 18.
All of my models conform to a protocol that guarantees they have a 'serverID' String variable.
I wrote a function that would allow me to pass in a serverID String and have it fetch the model object that matched. Because I am lazy and don't like writing the same functions over and over, I used a Self reference so that all of my conforming models get this static function.
Imagine my model is called "WhatsNew". Here's some code defining the protocol and the fetching function.
protocol RemotelyFetchable: PersistentModel {
var serverID: String { get }
}
extension WhatsNew: RemotelyFetchable {}
extension RemotelyFetchable {
static func fetchOne(withServerID identifier: String, inContext modelContext: ModelContext) -> Self? {
var fetchDescriptor = FetchDescriptor<Self>()
fetchDescriptor.predicate = #Predicate<Self> { $0.serverID == identifier }
do {
let allModels = try modelContext.fetch(fetchDescriptor)
return allModels.first
} catch {
return nil
}
}
}
Worked great! Or so I thought...
I built this and happily ran a debug build in the Simulator and on devices for months while developing the initial version but when I went to go do a release build for TestFlight, that build reliably crashed on every device with a message like this:
SwiftData/DataUtilities.swift:65: Fatal error: Couldn't find \WhatsNew. on WhatsNew with fields [SwiftData.Schema.PropertyMetadata(name: "serverID", keypath: \WhatsNew., defaultValue: nil, metadata: Optional(Attribute - name: , options: [unique], valueType: Any, defaultValue: nil, hashModifier: nil)), SwiftData.Schema.PropertyMetadata(name: "title", keypath: \WhatsNew., defaultValue: nil, metadata: nil), SwiftData.Schema.PropertyMetadata(name: "bulletPoints", keypath: \WhatsNew.)>, defaultValue: nil, metadata: nil), SwiftData.Schema.PropertyMetadata(name: "dateDescription", keypath: \WhatsNew., defaultValue: nil, metadata: nil), SwiftData.Schema.PropertyMetadata(name: "readAt", keypath: \WhatsNew.)>, defaultValue: nil, metadata: nil)]
It seems (cannot confirm) that something in the release build optimization process is stripping out some metadata / something about these models that makes this predicate crash.
Tested on iOS 18.0 and 18.1 beta.
How can I resolve this? I have two dozen types that conform to this protocol. I could manually specialize this function for every type myself but... ugh.
I believe the issue is related to a bug that the Swift compiler doesn’t correctly handle the dynamic Self
when it is used with a key path. I’d suggest that you file a feedback report (http://developer.apple.com/bug-reporting/) to voice that you are impacted, which may help the Swift folks to prioritize the work – If you do so, please share your report ID here.
In your case, you might work around the issue by avoiding using Self
with a key path, as shown below:
- Change
fetchOne
to accept a predicate:
static func fetchOne(witPredicate: Predicate<Self>, inContext modelContext: ModelContext) -> Self? {
var fetchDescriptor = FetchDescriptor<Self>()
fetchDescriptor.predicate = witPredicate
do {
let allModels = try modelContext.fetch(fetchDescriptor)
return allModels.first
} catch {
return nil
}
}
- Use
fetchOne
in the following way:
let identifier = whatsNew.serverID
let predicate = #Predicate<WhatsNew> {
$0.serverID == identifier
}
if let existingWhatsNew = WhatsNew.fetchOne(witPredicate: predicate, inContext: modelContext) {
whatsNew.readAt = existingWhatsNew.readAt
}
Best,
——
Ziqiao Chen
Worldwide Developer Relations.