Search code examples
iosswiftdrawinguibezierpath

Removing lagging latency in drawing UIBezierPath smooth lines in Swift


The code below draws smooth curved lines by overriding touches, but there is noticeable lagging or latency. The code uses addCurveToPoint and calls setNeedsDisplay after every 4 touch points which causes a jumpy appearance as the drawing doesn't keep up with finger movements. To remove the lagging or perceived latency, touch points 1, 2, 3 (leading up to touch point 4) could be temporarily filled with addQuadCurveToPoint and addLineToPoint.

  1. How can this actually be achieved in code to remove perceived lagging by using a temporary Line and QuadCurved line before displaying a final Curved line?

  2. If the below class is attached to one UIView (e.g. viewOne or self), how do I make a copy of the drawing to another UIView outside the class (e.g. viewTwo) after touchesEnded?

     //  ViewController.swift
    
    import UIKit
    
    class drawSmoothCurvedLinesWithLagging: UIView {
    
        let path=UIBezierPath()
        var incrementalImage:UIImage?
    
        var points = [CGPoint?](count: 5, repeatedValue: nil)
    
        var counter:Int?
    
        var strokeColor:UIColor?
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        override func drawRect(rect: CGRect) {
            autoreleasepool {
                incrementalImage?.drawInRect(rect)
                strokeColor = UIColor.blueColor()
                strokeColor?.setStroke()
                path.lineWidth = 20
                path.lineCapStyle = CGLineCap.Round
                path.stroke()
            }
        }
    
        override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
            counter = 0
    
            let touch: AnyObject? = touches.first
            points[0] = touch!.locationInView(self)
        }
    
        override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
            let touch: AnyObject? = touches.first
            let point = touch!.locationInView(self)
    
            counter = counter! + 1
            points[counter!] = point
    
    
            if counter == 2{
                //use path.addLineToPoint ?
                //use self.setNeedsDisplay() ?
            }
    
            if counter == 3{
                //use path.addQuadCurveToPoint ?
                //use self.setNeedsDisplay() ?
            }
    
            if counter == 4{
                points[3]! = CGPointMake((points[2]!.x + points[4]!.x)/2.0, (points[2]!.y + points[4]!.y)/2.0)
                path.moveToPoint(points[0]!)
                path.addCurveToPoint(points[3]!, controlPoint1: points[1]!, controlPoint2: points[2]!)
    
                self.setNeedsDisplay()
    
                points[0]! = points[3]!
                points[1]! = points[4]!
                counter = 1
            }
        }
    
        override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
            self.drawBitmap()
            self.setNeedsDisplay()
            path.removeAllPoints()
            counter = 0
        }
    
        override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
            self.touchesEnded(touches!, withEvent: event)
        }
    
        func drawBitmap(){
            UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
            strokeColor?.setStroke()
            if((incrementalImage) == nil){
                let rectPath:UIBezierPath = UIBezierPath(rect: self.bounds)
                UIColor.whiteColor().setFill()
                rectPath.fill()
            }
    
            incrementalImage?.drawAtPoint(CGPointZero)
            path.stroke()
            incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
        }
    
    }
    
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
    
    }
    

Solution

    1. Yes, adding a curve every few points will give it a stuttering lag. So, yes, you can reduce this affect by adding a line to points[1], adding a quad curve to points[2] and adding a cubic curve to points[3].

      As you said, make sure to add this to a separate path, though. So, in Swift 3/4:

      class SmoothCurvedLinesView: UIView {
          var strokeColor = UIColor.blue
          var lineWidth: CGFloat = 20
          var snapshotImage: UIImage?
      
          private var path: UIBezierPath?
          private var temporaryPath: UIBezierPath?
          private var points = [CGPoint]()
      
          override func draw(_ rect: CGRect) {
              snapshotImage?.draw(in: rect)
      
              strokeColor.setStroke()
      
              path?.stroke()
              temporaryPath?.stroke()
          }
      
          override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
              if let touch = touches.first {
                  points = [touch.location(in: self)]
              }
          }
      
          override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
              guard let touch = touches.first else { return }
              let point = touch.location(in: self)
      
              points.append(point)
      
              updatePaths()
      
              setNeedsDisplay()
          }
      
          private func updatePaths() {
              // update main path
      
              while points.count > 4 {
                  points[3] = CGPoint(x: (points[2].x + points[4].x)/2.0, y: (points[2].y + points[4].y)/2.0)
      
                  if path == nil {
                      path = createPathStarting(at: points[0])
                  }
      
                  path?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
      
                  points.removeFirst(3)
      
                  temporaryPath = nil
              }
      
              // build temporary path up to last touch point
      
              if points.count == 2 {
                  temporaryPath = createPathStarting(at: points[0])
                  temporaryPath?.addLine(to: points[1])
              } else if points.count == 3 {
                  temporaryPath = createPathStarting(at: points[0])
                  temporaryPath?.addQuadCurve(to: points[2], controlPoint: points[1])
              } else if points.count == 4 {
                  temporaryPath = createPathStarting(at: points[0])
                  temporaryPath?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
              }
          }
      
          override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
              finishPath()
          }
      
          override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
              finishPath()
          }
      
          private func finishPath() {
              constructIncrementalImage()
              path = nil
              setNeedsDisplay()
          }
      
          private func createPathStarting(at point: CGPoint) -> UIBezierPath {
              let localPath = UIBezierPath()
      
              localPath.move(to: point)
      
              localPath.lineWidth = lineWidth
              localPath.lineCapStyle = .round
              localPath.lineJoinStyle = .round
      
              return localPath
          }
      
          private func constructIncrementalImage() {
              UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
              strokeColor.setStroke()
              snapshotImage?.draw(at: .zero)
              path?.stroke()
              temporaryPath?.stroke()
              snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
              UIGraphicsEndImageContext()
          }
      }
      

      You could even marry this with iOS 9 predictive touches (as I described in my other answer), which could reduce lag even further.

    2. To take this resulting image and use it elsewhere, you can just grab the incrementalImage (which I renamed to snapshotImage, above), and drop it into an image view of the other view.

    For Swift 2 rendition, see previous revision of this answer.