Hi! I'm running into some confusing behavior when attempting to delete all instance of one model type from a ModelContext
. My problem is specifically using the delete(model:where:includeSubclasses:)
^1 function (and passing in a model type). I seem to be running into situations where this function fails silently without throwing an error (no models are deleted).
I am seeing this same behavior from Xcode_15.4.0
and Xcode_16_beta_4
.
I start with a model:
@Model final public class Item {
var timestamp: Date
public init(timestamp: Date = .now) {
self.timestamp = timestamp
}
}
Here is an example of a Store
class that wraps a ModelContext
:
final public class Store {
public let modelContext: ModelContext
public init(modelContainer: SwiftData.ModelContainer) {
self.modelContext = ModelContext(modelContainer)
}
}
extension Store {
private convenience init(
schema: Schema,
configuration: ModelConfiguration
) throws {
let container = try ModelContainer(
for: schema,
configurations: configuration
)
self.init(modelContainer: container)
}
}
extension Store {
public convenience init(url: URL) throws {
let schema = Schema(Self.models)
let configuration = ModelConfiguration(url: url)
try self.init(
schema: schema,
configuration: configuration
)
}
}
extension Store {
public convenience init(isStoredInMemoryOnly: Bool = false) throws {
let schema = Schema(Self.models)
let configuration = ModelConfiguration(isStoredInMemoryOnly: isStoredInMemoryOnly)
try self.init(
schema: schema,
configuration: configuration
)
}
}
extension Store {
public func fetch<T>(_ type: T.Type) throws -> Array<T> where T : PersistentModel {
try self.modelContext.fetch(
FetchDescriptor<T>()
)
}
}
extension Store {
public func fetchCount<T>(_ type: T.Type) throws -> Int where T : PersistentModel {
try self.modelContext.fetchCount(
FetchDescriptor<T>()
)
}
}
extension Store {
public func insert<T>(_ model: T) where T : PersistentModel {
self.modelContext.insert(model)
}
}
extension Store {
public func delete<T>(model: T.Type) throws where T : PersistentModel {
try self.modelContext.delete(model: model)
}
}
extension Store {
public func deleteWithIteration<T>(model: T.Type) throws where T : PersistentModel {
for model in try self.fetch(model) {
self.modelContext.delete(model)
}
}
}
extension Store {
private static var models: Array<any PersistentModel.Type> {
[Item.self]
}
}
That should be pretty simple… I can use this Store
to read and write Item
instances to a ModelContext
.
Here is an example of an executable that shows off the unexpected behavior:
func main() async throws {
do {
let store = try Store(isStoredInMemoryOnly: true)
store.insert(Item())
print(try store.fetchCount(Item.self) == 1)
try store.delete(model: Item.self)
print(try store.fetchCount(Item.self) == 0)
}
do {
let store = try Store(isStoredInMemoryOnly: true)
store.insert(Item())
print(try store.fetchCount(Item.self) == 1)
try store.deleteWithIteration(model: Item.self)
print(try store.fetchCount(Item.self) == 0)
}
do {
let store = try StoreActor(isStoredInMemoryOnly: true)
await store.insert(Item())
print(try await store.fetchCount(Item.self) == 1)
try await store.delete(model: Item.self)
print(try await store.fetchCount(Item.self) == 0)
}
do {
let store = try StoreActor(isStoredInMemoryOnly: true)
await store.insert(Item())
print(try await store.fetchCount(Item.self) == 1)
try await store.deleteWithIteration(model: Item.self)
print(try await store.fetchCount(Item.self) == 0)
}
}
try await main()
My first step is to set up an executable with an info.plist
to support SwiftData.^2
My expectation is all these print
statements should be true
. What actually happens is that the calls to delete(model:where:includeSubclasses:)
seem to not be deleting any models (and also seem to not be throwing errors).
I also have the option to test this behavior with XCTest
. I see the same unexpected behavior:
import XCTest
final class StoreXCTests : XCTestCase {
func testDelete() throws {
let store = try Store(isStoredInMemoryOnly: true)
store.insert(Item())
XCTAssert(try store.fetchCount(Item.self) == 1)
try store.delete(model: Item.self)
XCTAssert(try store.fetchCount(Item.self) == 0)
}
func testDeleteWithIteration() throws {
let store = try Store(isStoredInMemoryOnly: true)
store.insert(Item())
XCTAssert(try store.fetchCount(Item.self) == 1)
try store.deleteWithIteration(model: Item.self)
XCTAssert(try store.fetchCount(Item.self) == 0)
}
}
Those tests fail… implying that the delete(model:where:includeSubclasses:)
is not actually deleting any models.
FWIW… I see the same behavior (from command-line and XCTest
) when my Store
conforms to ModelActor
.^3 ^4
This does not seem to be the behavior I am seeing from using the delete(model:where:includeSubclasses:)
in a SwiftUI app.^5 Calling the delete(model:where:includeSubclasses:)
function from SwiftUI does delete all the model instances.
The SwiftUI app uses a ModelContext
directly (without a Store
type). I can trying writing unit tests directly against ModelContext
and I see the same behavior as before (no model instances are being deleted).^6
Any ideas about that? Is this a known issue with SwiftData that is being tracked? Is the delete(model:where:includeSubclasses:)
known to be "flaky" when called from outside SwiftUI? Is there anything about the way these ModelContext
instance are being created that we think is leading to this unexpected behavior?