1

I am playing with SwiftData in an iOS app using XCode 15 Beta 5. Really enjoying the experience so far, very easy to use. My question relates to how can I enable undo and redo functionality when editing an existing entity in a user entry form. The functionality I am hoping to achieve is after the user has made one or more changes to the managed object data, s/he should be able to press the undo button once to undo each input control to its original value in reverse order of the changes made. Likewise redo should restore the last undone value.

Here is the code for the user entry form:

import SwiftUI
import SwiftData 

struct TradingEntityEditor: View {
    @Environment(\.modelContext) private var context
    @Environment(\.dismiss) private var dismiss
    @Bindable var tradingEntity: TradingEntity

    @State private var autoSaveOnAppear = false
    var canUndo: Bool { context.undoManager?.canUndo ?? true }
    var canRedo: Bool { context.undoManager?.canRedo ?? true }

    var body: some View {
        VStack(spacing: 12) {
            LabeledContent("Name") {
                TextField("required", text: $tradingEntity.name)
                    .textFieldStyle(.roundedBorder)
            }
    
            LabeledContent("Address") {
                TextField("", text: $tradingEntity.address)
                    .textFieldStyle(.roundedBorder)
            }
        
            LabeledContent("Email") {
                TextField("email address", text: $tradingEntity.email)
                    .textFieldStyle(.roundedBorder)
            }
        
            LabeledContent("Telehone") {
                TextField("contact phone number", text: $tradingEntity.telephone)
                    .textFieldStyle(.roundedBorder)
            }
        
            Spacer()
        }
        .labeledContentStyle(TextFieldLabeledContentStyle())
        .padding()
        .toolbar{
            ToolbarItem(placement: .topBarTrailing) {
                Button {
                    // Perform undo last change
                    undoLastChange()
                } label: {
                    Label("undo changes", systemImage: "arrow.uturn.backward")
                }
                .disabled(!canUndo)
            }
            ToolbarItem(placement: .topBarTrailing) {
                Button {
                    // Perform redo last undo
                    redoLastUndo()
                } label: {
                    Label("undo changes", systemImage: "arrow.uturn.forward")
                }
                .disabled(!canRedo)
            }
        }
        .navigationTitle("Edit trading entity")
        .onAppear {
            appearAction()
        }
        .onDisappear {
            disappearAction()
        }
    }

    func appearAction() {
        UITextField.appearance().clearButtonMode = .whileEditing
        autoSaveOnAppear = context.autosaveEnabled
        context.autosaveEnabled = false
    }

    func disappearAction() {
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("Failed to save changes to current data")
            }
        }
        context.autosaveEnabled = autoSaveOnAppear
    }

    func undoLastChange() {
        guard let um = context.undoManager, um.canUndo else { return }
        um.undo()
    }

    func redoLastUndo() {
        guard let um = context.undoManager, um.canRedo else { return }
        um.redo()
    }
}

The undo operation doesn't perform the expected changes although some typed characters are removed from the last field. If anyone can suggest how this can be implemented, I would be most grateful and I am sure the answer will be useful to lots of developers.

EDIT For clarity, the functionality appears to be already available out-of-the-box by selecting the field to undo and shaking the device, 3-finger swipe (iPhone) or on-screen keyboard undo/redo button. The shake action also pops up a confirmation dialog. I want to implement a single button click which selects the last field edited and restores the original value.

apps2go
  • 86
  • 1
  • 6
  • Not sure about the 3 finger swipe etc., but `modelContext.undoManager?.beginUndoGrouping()` when opening eg. the edit sheet / the user starts editing the textfield and `modelContext.undoManager?.endUndoGrouping()` when closing / submitting it combined with `modelContext.undoManager?.undoNestedGroup()` should do the job. However for me the changes are not updated in the view until reopening the app. Maybe a bug or me being stupid. – lucaszischka Aug 12 '23 at 15:59

1 Answers1

0

How do you expect it to work? As you have it right now, the undo manager should undo every change on TradingEntity, that is, each keystroke on a TextField. Maybe you are expecting it to undo not every keystroke but the submitted value? Then you need to only update your entity with that final value.

private let tradingEntity: TradingEntity
@State private var localTradingEntity: TradingEntity

// Initialize the state with the entity
init(entity: TradingEntity) { ... }

...

var body: some View {
        VStack(spacing: 12) {
            LabeledContent("Name") {
                TextField("required", text: $localTradingEntity.name)
                    .onSubmit {
                       // Update values on your tradingEntity from localTradingEntity
                       copyValues()
                 }
            }
      }
}

Now every time you undo you are undoing the name and not every keystroke.

Affinity
  • 56
  • 9
  • I would like to restore each input control to its original value one at a time (for each press of the undo button) until the entity returns to its original state before commencing the edit. – apps2go Aug 08 '23 at 10:07
  • 1
    Why is this posted as an answer, it looks more like a comment to me? – Joakim Danielson Aug 08 '23 at 11:23
  • Added some code for your expected behavior. Not tested, but should work just fine. Another way of doing it would be with another ModelContext and merging changes, but haven't researched if that's possible in SwiftData yet. – Affinity Aug 09 '23 at 06:21
  • I don't think this is quite what I'm looking for. The undo/redo functionality is already built in to SwiftData. On the iPad the undo and redo buttons show on the on-screen keyboard toolbar. However, you have to select the field you want to undo, then press undo or redo. Undo restores the original value of the entity, redo restores the changed value. Similar functionality works on the iPhone by shaking the device or using 3-finger swipe. In the latter case, however, it also pops up a confirmation alert dialog (this also works on iPad). – apps2go Aug 09 '23 at 10:27
  • What I am hoping to achieve is a programmatic solution whereby each change is sequentially undone in reverse order of the user's changes. This may be a bit simplistic because the user may change the same field more than once. That's a problem I will address once I understand how to control the undo/redo process in code. – apps2go Aug 09 '23 at 10:33