Search code examples
uiviewuikitpencilkitpkcanvasview

PKCanvas mysteriously returns original drawing, how?


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 {
    }
  }
}

Solution

  • 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()
        }
    }