0

I was really comfortable using PencilKit in SwiftUI, however I revisited a project and apparently there is a bug in Xcodes new version where strokes disappear after drawing them in the simulator. I am running Version 14.1 of Xcode. I set up a minimal code example to show my problem:

import SwiftUI
import PencilKit

struct ContentView: View {
    var body: some View {
        PKCanvasRepresentation()
    }
}

struct PKCanvasRepresentation : UIViewRepresentable {
    func makeUIView(context: Context) -> PKCanvasView {
        var canvas = PKCanvasView()
        canvas.drawingPolicy = .anyInput
        return canvas
    }
    
    func updateUIView(_ uiView: PKCanvasView, context: Context) {
        
    }
}

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

enter image description here

This does not draw correctly in the preview or simulator but it works if I run it on a non-virtual testing device. Does anyone found a solution to this, as it makes debugging really uncomfortable.

jakob witsch
  • 157
  • 1
  • 1
  • 12

2 Answers2

0

this is an Xcode 14 bug that specifically happens with an iOS 16 simulator.

At first I suspected the hardware, but this was not confirmed. With the same project, hardware, Xcode and simulator, it is simply random whether it works.

0

I was confronted with the same problem and found a working solution for the PKCanvasView so that it can be properly used in the simulator.

In essence: In debug mode I replace the PKCanvasView by a subclass that adds an UIView with an image as a subview. The image is made from an UIBezierPath that is generated from the PKCanvasView' drawing. When drawing, the UIView updates its image from the PKCanvasView' drawing and draws itself.

Code: In the CanvasUIViewControllerRepresentable:

func makeUIViewController(context: Context) -> CanvasUIViewController {
          let theViewController = CanvasUIViewController()
          var view: PKCanvasView
    #if DEBUG
          view = CanvasView(frame: CGRect(origin: .zero, size: CGSize(width:820, height: 1180)))
          debugPrint("Using CanvasView")
    #else
          view = PKCanvasView(frame: CGRect(origin: .zero, size: CGSize(width:820, height: 1180)))
    #endif
          view.isOpaque = true
          view.backgroundColor = UIColor.systemGray6
          view.isUserInteractionEnabled = true
          view.isMultipleTouchEnabled = true
          view.autoresizesSubviews = true
          view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
          view.contentSize = CGSize(width: 1000, height: 1000)
          theViewController.view = view
          view.maximumZoomScale = 10
          view.minimumZoomScale = 0.1
          
          theViewController.view = view
          theViewController.canvasView = view
          
          theViewController.representedObject = self.canvasObject
          theViewController.delegate = context.coordinator
       
          return theViewController
       }
   

The class CanvasView:

class CanvasView: PKCanvasView {
       
       var imageView: CanvasImageView? = nil
       
       override func willMove(toWindow newWindow: UIWindow?) {
          let imageView = CanvasImageView(frame: CGRect(origin: .zero, size: self.contentSize))
          imageView.canvasView = self
          self.addSubview(imageView)
          imageView.backgroundColor = UIColor.clear
          imageView.isOpaque = false
          imageView.isUserInteractionEnabled = false
          self.imageView = imageView
       }
          
    
       override public func draw(_ rect: CGRect) {
          self.imageView?.setNeedsDisplay()
          super.draw(rect)
       }
    }

The CanvasImageView class:

class CanvasImageView: UIView {
       
       weak var canvasView: CanvasView? = nil 
       var image:UIImage? = nil
       
       
       override var canBecomeFirstResponder: Bool {
          return false
       }
       
       override public func draw(_ rect: CGRect) {
          if let canvasView = self.canvasView {
             debugPrint("Drawing ", canvasView.drawing.strokes.count, " Strokes", "zoomScale:", canvasView.zoomScale)
             let bezierPath = canvasView.drawing.bezierPath()
             
             let renderer = UIGraphicsImageRenderer(size: canvasView.contentSize)
             let image = renderer.image { (context) in
                UIColor.blue.setStroke()
                bezierPath.stroke()
             }
             let scaledImage = image.resizeImage(toWidth: (image.size.width * canvasView.zoomScale))
             self.image = scaledImage
          }
          self.image?.draw(at: .zero)
       }
    }

Extensions:

PKDrawing extension to generate a UIBezierPath:

import PencilKit

extension PKDrawing {
   
   func bezierPath() -> UIBezierPath {
      let theStrokes = self.strokes
      let bezierPath = UIBezierPath()
      
      theStrokes.forEach({ aStroke in
         let strokePath = aStroke.path
         let numberOfPoints = CGFloat(strokePath.count)
         let newBezierPath = UIBezierPath()
         var i: CGFloat = 0
         if numberOfPoints > 0 {
            let location = strokePath.interpolatedLocation(at: 0)
            newBezierPath.move(to: location)
         }
         if numberOfPoints > 1 {
            i = 1
            repeat {
               let location = strokePath.interpolatedLocation(at: i)
               newBezierPath.addLine(to: location)
               i += 1.0
            } while i < numberOfPoints
         }
         newBezierPath.lineWidth = 1.0
         bezierPath.append(newBezierPath)
      })
      
      return bezierPath
   }
   
}

The UIImage extension to scale an image:

func resizeImage(toWidth newWidth: CGFloat) -> UIImage? {
    
          let scale = newWidth/self.size.width
          if abs(scale - 1.0) < 0.01 { return self }
          let newHeight = self.size.height * scale
           UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
           self.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
    
           let newImage = UIGraphicsGetImageFromCurrentImageContext()
           UIGraphicsEndImageContext()
    
           return newImage
       }

In addition, I added some code to a PKCanvasViewDelegate methods:

//MARK: - PKCanvasViewDelegate
   func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
      self.isDrawingModified = true
      #if DEBUG
         self.canvasView.setNeedsDisplay()
      #endif
   }

The interaction with the CanvasView in the simulator is not perfect - but I believe that it is a workable solution.

M Wilm
  • 184
  • 7