PKCanvas mysteriously returns original drawing, how?

85 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
erotsppa 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
Brian Trzupek 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)