Edit: Thanks to some of the feedback, I have been able to get this partially working (updated code to reflect current changes).
Even though the app appears to be working as intended, I am still getting the 'Modifying state...' warning. How can I update the view's drawing in updateUIView
and push new drawings onto the stack with the canvasViewDrawingDidChange
without causing this issue? I have tried wrapping it in a dispatch call, but that just creates an infinite loop.
I'm trying to implement undo functionality in a UIViewRepresentable
(PKCanvasView
). I have a parent SwiftUI view called WriterView
which holds two buttons and the canvas.
Here's the parent view:
struct WriterView: View {
@State var drawings: [PKDrawing] = [PKDrawing()]
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
self.drawings = []
}
Button("Undo") {
if !self.drawings.isEmpty {
self.drawings.removeLast()
}
}
MyCanvas(drawings: $drawings)
}
}
}
Here is how I've implemented my UIViewRepresentable
:
struct MyCanvas: UIViewRepresentable {
@Binding var drawings: [PKDrawing]
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
uiView.drawing = self.drawings.last ?? PKDrawing()
}
func makeCoordinator() -> Coordinator {
Coordinator(self._drawings)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
@Binding drawings: [PKDrawing]
init(_ drawings: Binding<[PKDrawing]>) {
self._drawings = drawings
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
drawings.append(canvasView.drawing)
}
}
}
I am getting the following error:
[SwiftUI] Modifying state during view update, this will cause undefined behavior.
Presumably it is being caused by my coordinator's did change function, but I'm not sure how to fix this. What is the best way to approach this?
Thanks!
I finally (accidentally) figured out how to do this using UndoManager. I'm still not sure exactly why this works, because I never have to call self.undoManager?.registerUndo()
. Please comment if you understand why I never have to register an event.
Here's my working parent view:
struct Writer: View {
@Environment(\.undoManager) private var undoManager
@State private var canvasView = PKCanvasView()
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
canvasView.drawing = PKDrawing()
}
Button("Undo") {
undoManager?.undo()
}
Button("Redo") {
undoManager?.redo()
}
MyCanvas(canvasView: $canvasView)
}
}
}
Here's my working child view:
struct MyCanvas: UIViewRepresentable {
@Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
return canvasView
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
}
This certainly feels more like the intended approach for SwiftUI and is certainly more elegant than the attempts I made earlier.