The App: A very simple SwiftData app that populates two users, shows them in a List and then allows them to be deleted. The Seed Users buttons adds the users, Delete Users button uses modelContext.delete(model: Users.self)
to delete all of the users
Issue: Upon deleting all the models from the modelContext and then re-seeding the users, they will not delete after that. e.g. seed users, then delete works, then seed users and delete again does not work and the users stay persisted on disk.
Note1: If the app window is sent to the background by clicking a window from another app, and then brought forward with a click, delete works again.
Note2: I understand that at some point there was a SwiftData bug where .delete(model:
did not update the state of the ModelContext, but I think that was fixed and I also tried manually saving as well try! modelContext.save()
and the issue persists.
In this example, the users are being fetched from SwiftData and populate a users array, used in the List. Due to other constraints, @Query is not being used.
Here's the SwiftData user model
@Model
class User {
var userName: String
var favoriteFood: String
init(userName: String, favFood: String) {
self.userName = userName
self.favoriteFood = favFood
}
}
and the main entry point to the app
@main
struct SwiftUI_macOS_SwiftData_PrePopulatedApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: User.self)
}
}
and the ContentView
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var users: [User] = []
var body: some View {
VStack {
NavigationStack {
List(users) { user in
Text(user.userName)
}
.navigationTitle("Users")
}
Button("Seed Users") {
let jay = User(userName: "Jay", favFood: "Pizza")
let cindy = User(userName: "Cindy", favFood: "Steak")
modelContext.insert(jay)
modelContext.insert(cindy)
fetchUsers()
}
Button("Delete Users") {
try! modelContext.delete(model: User.self)
fetchUsers()
}
}
.padding()
.onAppear() {
fetchUsers()
}
}
func fetchUsers() {
do {
let descriptor = FetchDescriptor<User>(sortBy: [SortDescriptor(\.userName)])
users = try modelContext.fetch(descriptor)
} catch {
print("fetch failed: \(error.localizedDescription)")
}
}
}
Edit:
One other note for clarity: Run the app with no users, click Seed, which populates users, and then Delete them. Then Seed the users again, which appear then Delete again and quit the app. Then re-launch the app. The Users are still persisted and did not delete. e.g. the second time Delete is clicked after seeding, they do not delete.
I updated the delete code with this
Button("Delete Users") {
try! modelContext.delete(model: User.self)
try! modelContext.save()
if modelContext.hasChanges == true {
print("There are changes")
} else {
print("There are NO Changes")
}
fetchUsers()
}
With this change, There are NO changes outputs to console every time delete is clicked. However, if delete is clicked TWICE, they will delete upon second click.
Edit2:
From the documentation for autosave, which should eliminate the need to manually call save()
SwiftData automatically sets this property to true for the model container’s mainContext
so I would think Save
should never need to be called.
The short answer is to simply save the context after you insert:
Button("Seed Users") {
let jay = User(userName: "Jay", favFood: "Pizza")
let cindy = User(userName: "Cindy", favFood: "Steak")
//Insert users
modelContext.insert(jay)
modelContext.insert(cindy)
//Save context
try? modelContext.save() // <- Here, save before fetching
//Fetch users
fetchUsers()
}
This is needed because:
delete(model:where:includeSubclasses:)
Removes each model satisfying the given predicate from the persistent storage during the next save operation.
So .delete
will delete during the next save, meaning that without a save, it will not actually delete.
Differently put, it will delete whatever is in persistent storage during the next save operation. Without saving after insertion, there is nothing in persistent storage for .delete
to actually delete from there.
Because the objects are still in the context and not persisted, the fetch
will return them. You can prevent this from happening by using descriptor.includePendingChanges = false
to ensure the fetch descriptor only returns persisted objects (and ignores any that are pending in the yet unsaved context).
I don't know why you don't want to use a @Query
, but it should be noted that the documentation specifies that if used in a View
, the descriptor should be used with the @Query
macro:
If you’re displaying the fetched models in a SwiftUI view, use the descriptor with the
Query(_:animation:)
macro instead.
The model context can be queried to see the status of inserted, changed and deleted models, but without a Query, it seems the information will only be available if the fetch descriptor has includePendingChanges
set to true
.
Here's some sample code that includes some options that allow to experiment with all this:
import SwiftUI
import SwiftData
struct FoodiesContentView: View {
//Environment values
@Environment(\.modelContext) var modelContext
//State values
@State private var users: [User] = []
@State private var saveAfterInsert = false
@State private var includePendingChanges = true
//Body
var body: some View {
NavigationStack {
List(users) { user in
Text(user.userName)
}
.contentMargins(20)
.navigationTitle("Users")
.overlay {
if users.isEmpty {
ContentUnavailableView {
Label("No users", systemImage: "person.2.circle.fill")
}
}
}
.refreshable {
fetchUsers()
}
}
VStack(spacing: 20) {
HStack {
Text("Context has changes: \(modelContext.hasChanges ? "Yes" : "No") • Count: \(modelContext.insertedModelsArray.count)")
}
HStack {
Group {
Button("Seed") {
let jay = User(userName: "Jay", favFood: "Pizza")
let cindy = User(userName: "Cindy", favFood: "Steak")
//Insert users
modelContext.insert(jay)
modelContext.insert(cindy)
//Save context
if saveAfterInsert {
try? modelContext.save() // <- Here, save before fetching
}
//Fetch users
fetchUsers()
}
//Save context button
Button {
try? modelContext.save()
fetchUsers()
} label: {
Text("Save")
}
.tint(.mint)
//Reset context button
Button {
modelContext.rollback()
fetchUsers()
} label: {
Text("Reset context")
}
.tint(.yellow)
//Delete users button
Button("Delete", role: .destructive) {
try! modelContext.delete(model: User.self)
fetchUsers()
}
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, alignment: .center)
Divider()
//Toggles
VStack {
Toggle("Save after insert", isOn: $saveAfterInsert)
Toggle("Show unsaved users", isOn: $includePendingChanges)
}
.fixedSize()
.onAppear {
fetchUsers()
}
.onChange(of: includePendingChanges) {
fetchUsers()
}
}
.padding()
}
func fetchUsers() {
// print("fetching users - user count before fetch: \(users.count)")
do {
var descriptor = FetchDescriptor<User>(sortBy: [SortDescriptor(\.userName)])
descriptor.includePendingChanges = self.includePendingChanges
users = try modelContext.fetch(descriptor)
} catch {
print("fetch failed: \(error.localizedDescription)")
}
// print("fetching users - user count after fetch: \(users.count)")
}
}
@Model
class User {
var userName: String
var favoriteFood: String
init(userName: String, favFood: String) {
self.userName = userName
self.favoriteFood = favFood
}
}
#Preview {
FoodiesContentView()
.modelContainer(for: User.self, inMemory: false)
}