Search code examples
uiviewcore-graphicscalayeruibezierpathcashapelayer

How can i draw lines from touches in single CAShapeLayer to be able to select and move UIBezierPaths?


I'm trying to draw lines with touches and then be able to move it. i didn't use UIContext method with draw(_ rect: CGRect) for drawing because i wasn't able to get size of stroke and some of it's properties, so i used CAShapeLayer for drawing with touches methods like this:

    let shapeLayer: CAShapeLayer = {
    let layer = CAShapeLayer()
    layer.lineWidth = 1
    layer.strokeColor = UIColor.black.cgColor
    layer.fillColor = UIColor.clear.cgColor
    layer.lineCap = .round
    layer.lineJoin = .round
    layer.lineDashPattern = [10, 10]
    layer.name = "ShapeLayer"
    return layer
}()

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let shapeLayer = CAShapeLayer()
        shapeLayer.lineWidth = 1
        shapeLayer.strokeColor = UIColor.black.cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineCap = .round
        shapeLayer.lineJoin = .round
        shapeLayer.lineDashPattern = [10, 10]
        shapeLayer.name = "ShapeLayer"
        self.canvas.layer.addSublayer(shapeLayer)

        path = MyBezierPath()
        if let location = touches.first?.location(in: self.canvas) { previousTouchPoint = location }
}

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    guard let touch = touches.first?.location(in: self.canvas) else { return }
    
        if let location = touches.first?.location(in: self.canvas) {
            path.move(to: location)
            path.addLine(to: previousTouchPoint)
            previousTouchPoint = location

            if canvas.layer.sublayers != nil && canvas.layer.sublayers?.last?.name == "ShapeLayer" {
                guard let layer = canvas.layer.sublayers?.last as? CAShapeLayer else { return }
                print("Here \(layer.path?.boundingBoxOfPath)")
                layer.path = path.cgPath
            }
        }
 }

i was trying to add all UIBezierpath to single CAShapeLayer so i can select particular path and move it. But in this method it creates new CAShapeLayer for every line so i tried defining global CAShapeLayer variable and appending UIBezierPath for all lines and add it to global CAShapeLayer variable but it's very slow and laggy. is there any way i can draw with only one CAShapeLayer then be able to change location of it's BezierPath?


Solution

  • Here's a simple example of adding multiple "line segments" to a single UIBezierPath, and then being able to drag/move that path.

    In a UIView subclass, we:

    • create a UIBezierPath -- we'll call it thePath
    • create, assign properties, and add a CAShapeLayer -- shapeLayer
    • if we are in "Draw" mode
      • on touchesBegan, thePath.move(to: point)
      • on touchesMoved, thePath.addLine(to: point) and shapeLayer.path = thePath.cgPath
    • if we are in "Move" mode
      • on touchesBegan, save the point
      • on touchesMoved, transform the path

    On launch, it looks like this:

    enter image description here

    we touch-and-drag to add to the path:

    enter image description here

    a few more touch-and-drags to add more "segments":

    enter image description here

    now we switch to "Move" and drag down and to the right:

    enter image description here

    switch back to "Draw" and add a few more segments:

    enter image description here

    switch back to "Move" and drag up and to the left:

    enter image description here

    Example controller

    class DrawMoveLayerTestVC: UIViewController {
    
        let testView = DrawMoveLayerView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
            
            testView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
            testView.translatesAutoresizingMaskIntoConstraints = false
            
            // segmented control to switch between drawing / moving
            let segControl = UISegmentedControl(items: ["Draw", "Move"])
            segControl.translatesAutoresizingMaskIntoConstraints = false
            segControl.selectedSegmentIndex = 0
            segControl.addTarget(self, action: #selector(segChanged(_:)), for: .valueChanged)
            
            view.addSubview(testView)
            view.addSubview(segControl)
            
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // put segmented control at bottom
                segControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                segControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                segControl.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
    
                // constrain test view to all top/leading/trailing with 20-points "padding"
                testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                // bottom to segmented control top with 20-points "padding"
                testView.bottomAnchor.constraint(equalTo: segControl.topAnchor, constant: -20.0),
                
            ])
            
        }
        @objc func segChanged(_ sender: UISegmentedControl) {
            // set test view to "drawing" or "moving"
            testView.isDrawing = sender.selectedSegmentIndex == 0
        }
    }
    

    Example view subclass

    class DrawMoveLayerView: UIView {
        
        public var isDrawing: Bool = true
        
        private let shapeLayer = CAShapeLayer()
        private let thePath = UIBezierPath()
        private var startPoint: CGPoint = .zero
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            shapeLayer.lineWidth = 1
            shapeLayer.strokeColor = UIColor.black.cgColor
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.lineCap = .round
            shapeLayer.lineJoin = .round
            shapeLayer.lineDashPattern = [5, 10]
            shapeLayer.name = "ShapeLayer"
            layer.addSublayer(shapeLayer)
            self.clipsToBounds = true
        }
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let point = t.location(in: self)
            if isDrawing {
                thePath.move(to: point)
            } else {
                self.startPoint = point
            }
        }
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let point = t.location(in: self)
            if isDrawing {
                thePath.addLine(to: point)
            } else {
                // move the path by the distance the touch moved
                let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
                thePath.apply(tr)
                startPoint = point
            }
            // update the path of the shape layer
            shapeLayer.path = thePath.cgPath
        }
        
    }