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 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
fetchOneto 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
fetchOnein 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.