Step:
- Create and persist a model.
- Encode its persistentModelID using JSONEncoder.
- Decode the encoded data back into a PersistentIdentifier.
- Use the decoded identifier in a FetchDescriptor predicate to fetch the model.
- Modify a property of the model without saving the context.
- Execute the exact same fetch again, using the same decoded PersistentIdentifier and the same predicate.
Example:
do {
let item = Item()
modelContext.insert(item)
try modelContext.save()
// Encode and decode the persistent identifier.
// The decoded identifier is essential to reproduce the issue.
let data = try JSONEncoder().encode(item.persistentModelID)
let decodedPID = try JSONDecoder().decode(
PersistentIdentifier.self,
from: data
)
func find(_ pid: PersistentIdentifier) throws -> Item? {
let descriptor = FetchDescriptor<Item>(
predicate: #Predicate {
$0.persistentModelID == pid
}
)
return try modelContext.fetch(descriptor).first
}
// First fetch succeeds.
var result = try find(decodedPID)
print("0. Found: \(result != nil)") // true
// Modify the persisted model without saving the context.
item.timestamp = .now.addingTimeInterval(-3600)
// Second fetch uses exactly the same PID and predicate,
// but unexpectedly fails.
result = try find(decodedPID)
print("1. Found: \(result != nil)") // false
} catch {
print(error)
}
Actual Behavior:
The first fetch successfully finds the model.
After modifying one of its properties without saving the context, the second fetch returns an empty result, even though:
- The same ModelContext is used.
- The same decoded PersistentIdentifier is used.
- The same predicate is used.
- The model has not been deleted.
- No save operation has occurred between the two fetches.
Expected Behavior:
The second fetch should return the same model.
Changing an ordinary property of a model without saving should not invalidate its persistent identity or cause a predicate matching that identity to stop finding the model.
Additional Observations:
- If the original item.persistentModelID is used directly instead of the decoded identifier, the issue does not occur.
- If the model is not modified between the two fetches, both fetches succeed.
Questions:
- Is it expected that a PersistentIdentifier restored through Codable can be used in a SwiftData predicate?
- Why does a predicate using a decoded PersistentIdentifier stop matching a model after an unsaved property or relationship change?
- Is this a known issue or a limitation of using persistentModelID inside a #Predicate?
- What is the recommended way to persist a PersistentIdentifier and subsequently use it to fetch the corresponding model across different operations or app launches?