Search code examples
iosswiftuibezierpath

How to calculate the intersection of two UIBezierpaths


I need to calculate the intersection of two irregular UIBezierpaths say to closed paths representing irregular shapes. Problem is I don't even know how to get started on this. I've searched for libraries that could help but the only thing I found is a git repository missing files so it won't compile.


Solution

  • We can use Instance Methods on CGPath -- either retrieved from UIBezierPath or, a bit easier, working directly with CGMutablePath.

    See the Apple docs under Instance Methods here.

    Depending on what you really want to do, though, you might not need to manipulate your path at all. You can use it as a mask ... you can use path contains(_ point: CGPoint) ... and so on.

    But -- here is a really quick example of using CGPath intersection(_:using:)

    I'll make a few assumptions, such as you are adding an image view to a view subclass; tracking the touch began, moved, ended; using a CAShapeLayer to "draw" the path outline; etc.

    Looks kinda like this (tapping anywhere outside the gray frame will "clip" the path and fill it with green):

    enter image description here


    Simple UIView subclass

    • has a centered image view, with some padding on the sides (so we can "draw" outside the image)
    • draws a 2-point width path outline
    • closes the path on touches ended
    • has a public func to "clip" the drawn path with the image view's frame

    class MyCanvasView: UIView {
        
        let imgView = UIImageView()
        
        private var myPath: CGMutablePath!
        private var shapeLayer: CAShapeLayer = CAShapeLayer()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        private func commonInit() {
            imgView.translatesAutoresizingMaskIntoConstraints = false
            addSubview(imgView)
            
            NSLayoutConstraint.activate([
                imgView.topAnchor.constraint(equalTo: topAnchor, constant: 60.0),
                imgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 60.0),
                imgView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -60.0),
                imgView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -60.0),
            ])
            
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.strokeColor = UIColor.systemRed.cgColor
            shapeLayer.lineWidth = 2
            self.layer.addSublayer(shapeLayer)
            
            // in case there is no image set
            imgView.backgroundColor = .systemYellow
            self.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let pt = t.location(in: self)
            myPath = CGMutablePath()
            myPath.move(to: pt)
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.path = myPath
        }
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let pt = t.location(in: self)
            myPath.addLine(to: pt)
            shapeLayer.path = myPath
        }
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let pt = t.location(in: self)
            myPath.closeSubpath()
            shapeLayer.path = myPath
        }
    
        public func clipPath() {
            let imgPath = CGMutablePath(rect: imgView.frame, transform: nil)
            let clippedPath = imgPath.intersection(myPath)
            shapeLayer.path = clippedPath
            shapeLayer.fillColor = UIColor.green.cgColor
        }
    }
    

    Test View Controller class

    class TestPathsVC: UIViewController {
        
        let canvasView = MyCanvasView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            canvasView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(canvasView)
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                canvasView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                canvasView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                canvasView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                canvasView.heightAnchor.constraint(equalTo: canvasView.widthAnchor),
            ])
            
            if let img = UIImage(named: "testPic") {
                canvasView.imgView.image = img
            }
            
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            canvasView.clipPath()
        }
    }