Undo/Redo with DragGesture

I have a sample macOS app that I'm working on. I can run the exactly same lines of code below for iOS. For now, I'm running code for macOS since I can just press Command + z to undo the last action.

Anyway, I have two Text View objects. Since TextView has the DragGesture gesture, I am able to freely move either of them. And I want to undo and redo their positions. So the following is what I have.

import SwiftUI

struct ContentView: View {
	@State var textViews: [TextView] = [TextView(text: "George"), TextView(text: "Susan")]

	var body: some View {
		VStack {
			ForEach(textViews, id: \.id) { textView in
				textView
			}
		}
	}
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct TextView: View {
	@Environment(\.undoManager) var undoManager
	@StateObject private var undoModel = UndoViewModel()

	@State private var dragOffset: CGSize = .zero
	@State private var position: CGSize = .zero

	let id = UUID()
	let text: String
	init(text: String) {
		self.text = text
	}
	var body: some View {
		ZStack {
			Text(text)
				.fixedSize()
				.padding(.vertical, 10)
				.offset(x: dragOffset.width + position.width, y: dragOffset.height + position.height)
				.gesture(
					DragGesture()
						.onChanged {
							self.dragOffset = $0.translation
						}
						.onEnded( { (value) in
							self.position.width += value.translation.width
							self.position.height += value.translation.height
							self.dragOffset = .zero
							undoModel.registerUndo(CGSize(width: position.width, height: position.height), in: undoManager)
						})
				)
		}
	}
}

class UndoViewModel: ObservableObject {
	@Published var point = CGSize.zero

	func registerUndo(_ newValue: CGSize, in undoManager: UndoManager?) {
		let oldValue = point
		undoManager?.registerUndo(withTarget: self) { [weak undoManager] target in
			target.point = oldValue // registers an undo operation to revert to old text
			target.registerUndo(oldValue, in: undoManager) // this makes redo possible
		}
		undoManager?.setActionName("Move")
		point = newValue // update the actual value
	}
}

Well, if I press Command + z after moving one of them, it won't return to the last position. What am I doing wrong? Muchos thankos.

Hi Tomato, You started quite well.

If you add the following line as your TextView's body first for debugging help:

        let _ = Self._printChanges()

It will help you realize that when undoing, your textView's body is not recalculated. It means that nothing has changed for SwiftUI. Of course, undoModel.point did change, but this property is not used to compute the view's body, so a change won't trigger a refresh.

To check further you could mirror undoModel.point into the position property, on which the TextView's body depends, eg by adding the following modifier :

.onChange(of:undoModel.point) { _, newValue in
                    position = newValue
                }

and then undo works as expected.

This proposal only intends to point out what was missing. I assume that a complete fix would avoid copying from position to point and then back again to position, possibly by using a single property instead of 2.

Undo/Redo with DragGesture
 
 
Q