PKCanvas mysteriously returns original drawing, how?

79 Views Asked by At

MRE

I've spent so many hours trying to figure out whats happening but I cant figure it out

struct ContentView: View {
    @State private var canvasView = PKCanvasView()
    @State private var rendition = PKDrawing()

    func save() {
        rendition = canvasView.drawing
    }
    func load() {
        canvasView.drawing = rendition
    }
    func delete() {
        canvasView.drawing = PKDrawing()
    }
    var body: some View {
        VStack {
            Button {
                save()
            } label: {
                Text("Save")
            }
            Button {
                load()
            } label: {
                Text("load")
            }
            Button {
                delete()
            } label: {
                Text("delete")
            }
            CanvasView(canvasView: $canvasView)
        }
    }
}
  1. When I click save, the sketch is saved to memory.
  2. Then I continue sketching
  3. Then I press load to load a previously saved PKDrawing
  4. Then I resume drawing, and all of a sudden it reverts to the drawing done in (2)

What's going on?

struct CanvasView {
  @Binding var canvasView: PKCanvasView
  @State var toolPicker = PKToolPicker()
}

// MARK: - UIViewRepresentable
extension CanvasView: UIViewRepresentable {
  func makeUIView(context: Context) -> PKCanvasView {
    canvasView.tool = PKInkingTool(.pen, color: .gray, width: 10)
    #if targetEnvironment(simulator)
      canvasView.drawingPolicy = .anyInput
    #endif
    canvasView.delegate = context.coordinator
    showToolPicker()
    return canvasView
  }

  func updateUIView(_ uiView: PKCanvasView, context: Context) {}

  func makeCoordinator() -> Coordinator {
    Coordinator(canvasView: $canvasView)
  }
}

// MARK: - Private Methods
private extension CanvasView {
  func showToolPicker() {
    toolPicker.setVisible(true, forFirstResponder: canvasView)
    toolPicker.addObserver(canvasView)
    canvasView.becomeFirstResponder()
  }
}

// MARK: - Coordinator
class Coordinator: NSObject {
  var canvasView: Binding<PKCanvasView>

  // MARK: - Initializers
  init(canvasView: Binding<PKCanvasView>) {
    self.canvasView = canvasView
  }
}

// MARK: - PKCanvasViewDelegate
extension Coordinator: PKCanvasViewDelegate {
  func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
    if !canvasView.drawing.bounds.isEmpty {
    }
  }
}
2

There are 2 best solutions below

0
On BEST ANSWER

For others that encounter this. This solution is given to me from another forum.


I would try giving the CanvasView a binding or regular property for the PKDrawing rather than the PKCanvasView, and initialize a PKCanvasView and set it’s drawing in makeUIView. No idea if that’s your root cause but it is definitely wonky to own a UIKit view as a @State var in your content view. You can also save and load by adding a second @State var of type PKDrawing, and assigning the working drawing to or from the backup / “saved” drawing

Edit: it’d need to be a binding PKDrawing because the Controller delegate would need to update the ContentView’s @State drawing via the binding whenever the delegate method flags that the drawing changed.

In addition to the changes above, I found that PKCanvasView was somehow preserving the previous drawing even though we're explicitly setting the drawing. I came up with a wonky fix:

Add a @State var id = 0

Add a .id(id) to the CanvasView

After restoring the drawing in load(), increment id with id += 1 This will cause a new PKCanvasView to be created every time you tap Load

import SwiftUI

import PencilKit

struct ContentView: View {
    @State private var rendition = PKDrawing()
    @State private var backup = PKDrawing()
    @State private var strokes = 0

    func save() {
        backup = rendition
    }
    func load() {
        rendition = backup
    }
    func delete() {
        rendition = PKDrawing()
    }
    var body: some View {
        VStack {
            Button {
                save()
            } label: {
                Text("Save")
            }
            Button {
                load()
                strokes += 1
            } label: {
                Text("load")
            }
            Button {
                delete()
            } label: {
                Text("delete")
            }
            CanvasView(rendition: $rendition)
                .id(strokes)
        }
    }
}

struct CanvasView {
    @Binding var rendition: PKDrawing
    var toolPicker = PKToolPicker()
}

// MARK: - UIViewRepresentable
extension CanvasView: UIViewRepresentable {
    func makeUIView(context: Context) -> PKCanvasView {
        print("made view!")
        let canvasView = PKCanvasView()
        canvasView.drawing = rendition
        canvasView.tool = PKInkingTool(.pen, color: .gray, width: 10)
#if targetEnvironment(simulator)
        canvasView.drawingPolicy = .anyInput
#endif
        canvasView.delegate = context.coordinator
        showToolPicker(canvasView: canvasView)
        return canvasView
    }
    
    func updateUIView(_ uiView: PKCanvasView, context: Context) {
        uiView.delegate = nil
        uiView.drawing = rendition
        uiView.delegate = context.coordinator
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
}

// MARK: - Private Methods
private extension CanvasView {
    func showToolPicker(canvasView: PKCanvasView) {
        toolPicker.setVisible(true, forFirstResponder: canvasView)
        toolPicker.addObserver(canvasView)
        canvasView.becomeFirstResponder()
    }
}

// MARK: - Coordinator
class Coordinator: NSObject {
    let parent: CanvasView

    // MARK: - Initializers
    init(parent: CanvasView) {
        self.parent = parent
    }
}

// MARK: - PKCanvasViewDelegate
extension Coordinator: PKCanvasViewDelegate {
    func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
        DispatchQueue.main.async {
            self.parent.rendition = canvasView.drawing
        }
    }
}

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

When you save, you are saving to rendition.

When you load, you are loading from rendition.

So you are showing the drawing that you saved, when you hit load.

I suspect, you probably want load to actually load a 'rendition' from some other source (disk, database, etc)