4

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.
        }
    
    
    }
    
Rob
  • 415,655
  • 72
  • 787
  • 1,044
user4806509
  • 2,925
  • 5
  • 37
  • 72

1 Answers1

3
  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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • thank you very much. Brilliant! Appreciate your clear explanations. I will try grabbing the `snapshotImage`. I have noticed though two artefacts in the code. 1 - When drawing, lifting the finger, then beginning drawing again, it appears the last part of the previous drawing is removed and replaced with a thin line. This only occurs sometimes. Image: http://i.imgur.com/LIpfF4q.png 2 - A flat squared off corner appears when drawing. This also only occurs sometimes. Image: http://i.imgur.com/QyWpXG0.png What is causing these artefacts? – user4806509 Jan 26 '16 at 16:06
  • 1
    Good catch. Yes, the issue is that you were configuring the attributes only when `drawRect` was called, but `constructIncrementalImage` doesn't. But, the configuring of the `UIBezierPath` really doesn't belong in `drawRect` at all. It really should be set when the path is instantiated. So, I wrote a method to instantiate and configure a path, and use that whenever I need to create a path. See revised answer above. – Rob Jan 26 '16 at 16:52
  • I have noticed one other issue regarding accumulated lagging latency during continuous drawing that your insights might be able to help solve - https://stackoverflow.com/questions/35067811 – user4806509 Jan 28 '16 at 17:19
  • Would it be possible for you to briefly explain what is occurring in the section of code above when `pointCount == 5` and how it compares with `pointCount == 3`, both of which use `addCurveToPoint` quad bezier curves? – user4806509 Jan 29 '16 at 14:12
  • Thanks. I like the idea of simplified code but I’ve noticed since the change, sometimes a double line is present at the end of drawing, image: http://i.imgur.com/vU96UT5.png To try track down the issue, I’ve set lineWidth: CGFloat = 1 and added print(points.count) to the beginning of touchesEnded. Turns out when touching up when points.count = 2 the double line presents but I can’t work out why that is comparing the previous code. (It doesn’t happen on 3 and 4.) Any ideas on what it could be? – user4806509 Feb 04 '16 at 17:08
  • Sorry, @Rob. Let me correct myself. Yes, I'm using the exact same code above. All I have changed is `lineWidth: CGFloat = 1` and I have added `print(points.count)` just under `private func updatePaths() {`. When I run this, do lots of short drawings, it shows that when `points.count = 5`, a double line is produced. http://pastie.org/private/bjifufdrnpljt9xscfcogg – user4806509 Feb 04 '16 at 19:11
  • @user4806509 - We can simplify further (getting rid of `pointsCount`) and the issue is remedied. See revised answer – Rob Feb 05 '16 at 08:15
  • Brilliant! Thanks for your help and patience @Rob. (I posted a different type of question yesterday about scale affine transformations if you are interested in looking at that. http://stackoverflow.com/questions/35207177/scale-affine-transformation-scaling-differently-on-ios7-and-ios8-ios9-in-swift ) Thanks again. Cheers. – user4806509 Feb 05 '16 at 15:54
  • Hi @Rob Thanks again. I have a new problem based on this code regarding adding an eraser function. If you are interested in looking at it: http://stackoverflow.com/questions/35511211 – user4806509 Feb 19 '16 at 17:29