swiftuibezierpathcgrecttouchesbeganuigraphicscontext

draw a straight line with a starting touch point


I want my swift code to draw a straight horiziontal line. Right now it selects a point and the user extends the line anchored in the first point. I just want the user to be able to draw the line left or right. I tried alternating bezier.addLine(to: CGPoint(x:startTouch!.x, y:startTouch!.y)) but that does not seem to have any effect.

import UIKit

class ViewController2: UIViewController {

    @IBOutlet weak var drawingPlace: UIImageView!

    var startTouch : CGPoint?
    var secondTouch : CGPoint?
    var currentContext : CGContext?
    var prevImage : UIImage?


    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemOrange
        drawingPlace.backgroundColor = .gray
    }


    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        startTouch = touch?.location(in: drawingPlace)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {

        for touch in touches{
            secondTouch = touch.location(in: drawingPlace)

            if(self.currentContext == nil){
                UIGraphicsBeginImageContext(drawingPlace.frame.size)
                self.currentContext = UIGraphicsGetCurrentContext()
            }else{
                self.currentContext?.clear(CGRect(x: 0, y: 0, width: drawingPlace.frame.width, height: drawingPlace.frame.height))
            }

            self.prevImage?.draw(in: self.drawingPlace.bounds)

            let bezier = UIBezierPath()

            bezier.move(to: startTouch!)
            bezier.addLine(to: secondTouch!)
            
            bezier.addLine(to: CGPoint(x:startTouch!.x, y:startTouch!.y))
            bezier.close()

            UIColor.blue.set()

            self.currentContext?.setLineWidth(4)
            self.currentContext?.addPath(bezier.cgPath)
            self.currentContext?.strokePath()
            let img2 = self.currentContext?.makeImage()
            drawingPlace.image = UIImage.init(cgImage: img2!)

        }
    }


    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

        self.currentContext = nil
        self.prevImage = self.drawingPlace.image
    }

}

Solution

  • If you want to draw a horizontal line, create a CGPoint whose x is the location of the touch and whose y is that of the starting point. That will result in a horizontal line.


    That having been said, here are a few other of observations:

    1. If you call UIGraphicsBeginImageContext, you must call UIGraphicsEndImageContext. You should do this within touchesMoved, not trying to hang on to the context beyond this call.

    2. If you were to do this, we would generally use UIGraphicsImageRenderer nowadays.

    3. Personally, I wouldn't try rendering an image for every touch. That is a pretty expensive operation. I would just add a CAShapeLayer and then update the path for that layer. Let CAShapeLayer take care of the rendering of the path.

    4. I'm not quite sure why you are iterating through the array of touches. I would just grab one and use that.

    5. I might suggest using predictive touches to minimize perceived lagginess.

    6. The startTouch is actually a CGPoint, not a UITouch, so I might call it startPoint instead.

    7. If you want to make a snapshot image, I'd do that in touchesEnded, not in touchesMoved.

    For example:

    class ViewController: UIViewController {
        @IBOutlet weak var imageView: UIImageView!
        
        var startPoint: CGPoint?
    
        let shapeLayer: CAShapeLayer = {
            let shapeLayer = CAShapeLayer()
            shapeLayer.lineWidth = 4
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.strokeColor = UIColor.blue.cgColor
            return shapeLayer
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            imageView.backgroundColor = .systemOrange
            imageView.layer.addSublayer(shapeLayer)
        }
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            let touch = touches.first
            startPoint = touch?.location(in: imageView)
        }
    
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard var touch = touches.first else { return }
    
            if let predicted = event?.predictedTouches(for: touch)?.last {
                touch = predicted
            }
    
            updatePath(in: imageView, to: touch)
        }
    
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first else { return }
            updatePath(in: imageView, to: touch)
            let image = UIGraphicsImageRenderer(bounds: imageView.bounds).image { _ in
                imageView.drawHierarchy(in: imageView.bounds, afterScreenUpdates: true)
            }
            shapeLayer.path = nil
            imageView.image = image
        }
    }
    
    private extension ViewController {
        func updatePath(in view: UIView, to touch: UITouch) {
            let point = touch.location(in: view)
            guard view.bounds.contains(point) else { return }
    
            let bezier = UIBezierPath()
    
            bezier.move(to: startPoint!)
            bezier.addLine(to: CGPoint(x: point.x, y: startPoint!.y))
    
            shapeLayer.path = bezier.cgPath
        }
    }
    

    This just renders the current line as a CAShapeLayer, but when the stroke is done, it creates a snapshot of the image view (permanently capturing the stroke in an image), removes the shape layer’s path, and updates the image view’s image.

    But hopefully this answers the question on how to make the line horizontal.