[SwiftData] Bugs with `autosaveEnabled` and `undoManager` + Observation

Hello,

my goal is it to implement an Edit Sheet of a Model with Discard changes in SwiftData.

  • Xcode Version 15.0 beta 6 (15A5219j)

Approaches

In the following modelContext refers to the context fetched from the environment by the View. Also code for showing the sheet (eg. toggling isPresented) is omitted.

// App
ContentView()
    .modelContainer(for: Model.self, isAutosaveEnabled: true, isUndoEnabled: true)
// In Content View
@Query var models: [Model]
//...
ForEach(models) { model in
    ModelView(model: model)
        //eg. longpresgesture that calls openEditSheet
        .sheet(..., content: {
            EditModelView(model: model)
        }
}

Disabling autosave + rollback

// In ContentView
func openEditSheet() {
    modelContext.autosaveEnabled = false
}

// In EditModelView
func discardEditSheet() {
    modelContext.rollback()
    modelContext.autosaveEnabled = true
}

func saveEditSheet() {
    try? modelContext.save() // probably not needed if autosave gets enabled anyway
    modelContext.autosaveEnabled = true
}

However this approach does not work, as SwiftData continues to save anyway and therefore there is nothing to rollback.

modelContext in Memory

// In ContentView
let context = ModelContext(modelContext.container)
context.autosaveEnabled = false
context.container.configuration.removeAll()
context.container.configuration.insert(ModelConfiguration(..., inMemory: true)
//...
EditModelView()
    .modelContext(context)
    // Also tried: .enviroment(\.modelContext, context)

// In EditModelView
func discardEditSheet() {
    modelContext.rollback() // probably not needed as the container is never saved
}

func saveEditSheet() {
    try? modelContext.save() // move to persistent storage from memory
}
  • The idea was to use something like parent in CoreData. However as this is (currently) not supported.
  • Also tried this by modifying the modelContext directly (instead of creating a new context) and its container directly or before creating the context.
  • The child can not be a ModelContainer as you can not pass a context to it or set its mainContext (get-only).

However this approach does not work, as the container does not seem to stay in memory. Maybe related to previous.

BackingData

EditModelView(model: Model(backingData: model.persistentBackingData)
  • not sure about this one, played around a little with it but not sure what it means / should mean and how I would expect it to work. I have never been great at cooking :).

UndoManager (preferred)

// In ContentView
func openEditSheet() {
    modelContext.undoManager?.beginUndoGrouping()
}

// In EditModelView
func discardEditSheet() {
    modelContext.undoManager?.endUndoGrouping()
    modelContext.undoManager?.undoNestedGroup()
    // Also tried adding: try? modelContext.save()
}

func saveEditSheet() {
    modelContext.undoManager?.endUndoGrouping()
}

This approach does works kinda weird:

  • The ModelView of the updated model in ContentView's ForEach is not updated regarding the undo action.
  • However when I re-open the EditTaskView of the updated model it has the expected state, even though the model to edit is passed by the ContentView (which does not have the correct state?).
  • After relaunching the app or when modifying the model again (this time not undoing) the previous sate changes are recognised

Therefore this looks like it does what I want, with the ContentView having the correct state, but not displaying it. Only reason I can think of is the Model: Observable not getting triggered and therefore no View update.

Conclusion

After playing around with the above approaches and combining them in every possible way, i think that:

  • disabling autosave at runtime is currently not working and this is a bug (otherwise autosaveEnabled should be get-only).
  • UndoManager does not trigger a View update of a Model/Observable and this is a bug

Would be happy to hear other opinions on this. Am I missing something here? Am I ******?

Question

However as my conclusion does not fix my problem I am wondering:

  • Are my approaches (theoretically) correct?
  • How do I fix / workaround the UndoManager issue? If I identified it correctly, how can I manually notify the view that a Observable model has changed?

Hello,

Version 15.0 beta 6 (15A5219j)

It doesn't save if you use isAutosaveEnabled: false. At first glance, I see what you described that the object in a list was changed, but if you terminate your app and open it again, you will see the date wasn't changed. Of course, if you didn't use modelContext.save()

So the bug is here, I suppose.

I have observed the same thing.

In a view where I insert a new data, I have:

        @Environment(\.modelContext) private var modelContext
        ...

        .onAppear {
            modelContext.autosaveEnabled = false
        }

Yet when I terminate the app and re-open it, I notice the data did indeed get into persistent storage.

Update

After more testing I discovered the root of the problem:

Setting autosaveEnabled on the modelContext does not persist from one screen to the next.

For example:

  1. Say when ParentView loads you turn off autosaveEnabled in the onAppear (like the code above).
  2. Then navigate to DetailView.
  3. Observe that autosaveEnabled is set back to true now on DetailView.

I would have thought it would have persisted after I changed it but it did not persist.

I was experiencing the same issue, but I managed to put together an admittedly "hacky" workaround--it does seem to work, though. The basic idea is to call undoNestedGroup to cancel all the changes the user might have made, but then make a subtle change on the model object in code, revert the change, and then call save on the context. This will force the model to think that there was a change, and it will update the parent form. Here's some code that illustrates the approach that I put in the Cancel button on the action sheet where the object is edited:

undoManager?.endUndoGrouping()
undoManager?.undoNestedGroup()
let title = request.title
request.title = request.title + " "
request.title = title
try! context.save()`

Hopefully this issue will get resolved or someone will come up with a much more elegant solution, but maybe this will work in the meantime.

[SwiftData] Bugs with `autosaveEnabled` and `undoManager` + Observation
 
 
Q