skip to Main Content

I simply do the following in code:

    let path = UIBezierPath(rect: blurView.bounds)
    path.usesEvenOddFillRule = true
    path.append(UIBezierPath(rect: CGRect(x: 100, y: 100, width: 100, height: 100)))
    path.append(UIBezierPath(rect: CGRect(x: 150, y: 150, width: 100, height: 100)))
    //here you can add more paths, but the number is not known
    let layer = CAShapeLayer()
    layer.path = path.cgPath
    layer.fillRule = .evenOdd
    blurView.layer.mask = layer

and the effect is following:

enter image description here

Two rectangles overlapping one another. But all I need is to combine area from both rectanges, not to exclude everlapping area. Is it possible?

2

Answers


  1. Using the "even-odd" fill rule is great for "cutting a hole" in a path. However, this code:

    // create a big rect
    let path = UIBezierPath(rect: blurView.bounds)
    // cut a hole in it
    path.append(UIBezierPath(rect: CGRect(x: 100, y: 100, width: 100, height: 100)))
    // cut a hole overlapping a hole?
    path.append(UIBezierPath(rect: CGRect(x: 150, y: 150, width: 100, height: 100)))
    

    will be, as you’ve seen, problematic.

    Depending on what all you are wanting to do, you could use a library such as ClippingBezier which allows you to manipulate paths with boolean actions.

    Or, you can use a custom CALayer like this to "invert" multiple paths to use as a "cutout mask":

    class BasicCutoutLayer: CALayer {
        
        var rects: [CGRect] = []
        
        func addRect(_ newRect: CGRect) {
            rects.append(newRect)
            setNeedsDisplay()
        }
        func reset() {
            rects = []
            setNeedsDisplay()
        }
        
        override func draw(in ctx: CGContext) {
            
            // fill entire layer with solid color
            ctx.setFillColor(UIColor.gray.cgColor)
            ctx.fill(self.bounds);
    
            rects.forEach { r in
                ctx.addPath(UIBezierPath(rect: r).cgPath)
            }
    
            // draw clear "cutouts"
            ctx.setFillColor(UIColor.clear.cgColor)
            ctx.setBlendMode(.sourceIn)
            ctx.drawPath(using: .fill)
            
        }
        
    }
    

    To show it in use, we’ll use this image:

    In a standard UIImageView, overlaid with a blur UIVisualEffectView, and then use the BasicCutoutLayer class with two overlapping rects as the blur view’s layer mask:

    class BasicCutoutVC: UIViewController {
        
        let myBlurView = UIVisualEffectView()
        let myCutoutLayer = BasicCutoutLayer()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBlue
    
            let imgView = UIImageView()
            if let img = UIImage(named: "sampleBG") {
                imgView.image = img
            }
            
            [imgView, myBlurView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                imgView.topAnchor.constraint(equalTo: g.topAnchor),
                imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
                
                myBlurView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                myBlurView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                myBlurView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                myBlurView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
                
            ])
    
            myBlurView.effect = UIBlurEffect(style: .extraLight)
            
            // set mask for blur view
            myBlurView.layer.mask = myCutoutLayer
        }
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
        
            // set mask layer frame
            myCutoutLayer.frame = myBlurView.bounds
            
            // add two overlapping rects
            
            let v: CGFloat = 160
            let c: CGPoint = CGPoint(x: myBlurView.bounds.midX, y: myBlurView.bounds.midY)
            var r: CGRect = CGRect(origin: c, size: CGSize(width: v, height: v))
    
            r.origin.x -= v * 0.75
            r.origin.y -= v * 0.75
            myCutoutLayer.addRect(r)
    
            r.origin.x += v * 0.5
            r.origin.y += v * 0.5
            myCutoutLayer.addRect(r)
        }
    
    }
    

    Before applying the mask, it looks like this:

    enter image description here

    after applying the mask we get:

    enter image description here

    As we see, the "overlap" displays as we want.

    That was a very simple, basic example. For a more advanced example, take a look at this:

    struct MyPath {
        var lineWidth: CGFloat = 0
        var lineCap: CGLineCap = .butt
        var lineJoin: CGLineJoin = .bevel
        var isStroked: Bool = true
        var isFilled: Bool = true
        var pth: UIBezierPath = UIBezierPath()
    }
    
    class AdvancedCutoutLayer: CALayer {
        
        var myPaths: [MyPath] = []
        
        func addPath(_ newPath: MyPath) {
            myPaths.append(newPath)
            setNeedsDisplay()
        }
        func reset() {
            myPaths = []
            setNeedsDisplay()
        }
        
        override func draw(in ctx: CGContext) {
            
            // fill entire layer with solid color
            ctx.setFillColor(UIColor.gray.cgColor)
            ctx.fill(self.bounds);
            ctx.setBlendMode(.sourceIn)
    
            myPaths.forEach { thisPath in
                ctx.setStrokeColor(thisPath.isStroked ? UIColor.clear.cgColor : UIColor.black.cgColor)
                ctx.setFillColor(thisPath.isFilled ? UIColor.clear.cgColor : UIColor.black.cgColor)
                ctx.setLineWidth(thisPath.isStroked ? thisPath.lineWidth : 0.0)
                ctx.setLineCap(thisPath.lineCap)
                ctx.setLineJoin(thisPath.lineJoin)
                ctx.addPath(thisPath.pth.cgPath)
                ctx.drawPath(using: .fillStroke)
            }
            
        }
        
    }
    

    along with a subclassed UIVisualEffectView for convenience:

    class CutoutBlurView: UIVisualEffectView {
        
        let sl = AdvancedCutoutLayer()
        
        override init(effect: UIVisualEffect?) {
            super.init(effect: effect)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            sl.isOpaque = false
            layer.mask = sl
        }
        override func layoutSubviews() {
            super.layoutSubviews()
            sl.frame = bounds
            sl.setNeedsDisplay()
        }
        func addPath(_ newPath: MyPath) {
            sl.addPath(newPath)
        }
        func reset() {
            sl.reset()
        }
    }
    

    and an example controller:

    class AdvancedCutoutVC: UIViewController {
        
        let myView = CutoutBlurView()
        
        var idx: Int = 0
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBlue
            
            let imgView = UIImageView()
            if let img = UIImage(named: "sampleBG") {
                imgView.image = img
            }
            
            [imgView, myView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                imgView.topAnchor.constraint(equalTo: g.topAnchor),
                imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
                
                myView.topAnchor.constraint(equalTo: g.topAnchor),
                myView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                myView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                myView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
    
            ])
            
            myView.effect = UIBlurEffect(style: .extraLight)
            
        }
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
    
            Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true, block: { _ in
                switch self.idx % 4 {
                case 1:
                    self.addSomeOvals()
                case 2:
                    self.addSomeLines()
                case 3:
                    self.addSomeShapes()
                default:
                    self.addSomeRects()
                }
                self.idx += 1
            })
        }
        func addSomeRects() {
            myView.reset()
            let w: CGFloat = myView.frame.width / 4.0
            let h: CGFloat = myView.frame.height / 4.0
            var x: CGFloat = ((myView.frame.width - (w * 5.0 * 0.5)) * 0.5) - (w * 0.25)
            var y: CGFloat = ((myView.frame.height - (h * 5.0 * 0.5)) * 0.5) - (h * 0.25)
            for _ in 1...5 {
                let bz = UIBezierPath(rect: CGRect(x: x, y: y, width: w, height: h))
                myView.addPath(MyPath(lineWidth: 0, isStroked: false, isFilled: true, pth: bz))
                x += w * 0.5
                y += h * 0.5
            }
        }
        func addSomeOvals() {
            myView.reset()
            let w: CGFloat = myView.frame.width / 4.0
            let h: CGFloat = myView.frame.height / 4.0
            var x: CGFloat = ((myView.frame.width - (w * 5.0 * 0.5)) * 0.5) - (w * 0.25)
            var y: CGFloat = ((myView.frame.height - (h * 5.0 * 0.5)) * 0.5) - (h * 0.25)
            for _ in 1...5 {
                let bz = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: w, height: h))
                myView.addPath(MyPath(lineWidth: 0, isStroked: false, isFilled: true, pth: bz))
                x += w * 0.5
                y += h * 0.5
            }
        }
        func addSomeLines() {
            myView.reset()
            let w: CGFloat = myView.frame.width / 2.0
            let h: CGFloat = myView.frame.height / 4.0
            let x: CGFloat = 80
            var y: CGFloat = 80
            var lw: CGFloat = 4
            for _ in 1...5 {
                let bz = UIBezierPath()
                bz.move(to: CGPoint(x: x, y: y))
                bz.addLine(to: CGPoint(x: x + w, y: y + 20))
                myView.addPath(MyPath(lineWidth: lw, lineCap: .round, isStroked: true, isFilled: false, pth: bz))
                y += h * 0.5
                lw += 10
            }
        }
        func addSomeShapes() {
            myView.reset()
            var bz: UIBezierPath!
            
            bz = UIBezierPath(rect: CGRect(x: 80, y: 80, width: 80, height: 120))
            myView.addPath(MyPath(isStroked: false, isFilled: true, pth: bz))
    
            bz = UIBezierPath(rect: CGRect(x: 120, y: 120, width: 120, height: 60))
            myView.addPath(MyPath(isStroked: false, isFilled: true, pth: bz))
    
            bz = UIBezierPath(rect: CGRect(x: 80, y: 220, width: 220, height: 60))
            myView.addPath(MyPath(lineWidth: 12, isStroked: true, isFilled: false, pth: bz))
            
            bz = UIBezierPath(ovalIn: CGRect(x: 100, y: 240, width: 220, height: 60))
            myView.addPath(MyPath(lineWidth: 12, isStroked: true, isFilled: false, pth: bz))
    
            var r: CGRect = CGRect(x: 40, y: 320, width: myView.frame.width - 80, height: 200)
            for _ in 1...4 {
                bz = UIBezierPath(rect: r)
                myView.addPath(MyPath(lineWidth: 8, isStroked: true, isFilled: false, pth: bz))
                r = r.insetBy(dx: 20, dy: 20)
            }
        }
    }
    

    When run, this example will cycle through overlapping rect, overlapping ovals, some varying width lines, and some assorted shapes (just to give an idea):

    enter image description here enter image description here

    enter image description here enter image description here

    Login or Signup to reply.
  2. I would go with ClippingBezier because it is fast, easy to use and neat. It’ll be something like this:

    let rect1 = CGRect(x: 100, y: 100, width: 200, height: 200)
    let rect2 = CGRect(x: 150, y: 200, width: 200, height: 200)
            
    let path0 = UIBezierPath(rect: blurView.bounds)
    let path1 = UIBezierPath(rect: rect1)
    let path2 = UIBezierPath(rect: rect2)
            
    let unionPathArray = path1.union(with: path2)
    let unionPath = UIBezierPath()
            
    if let array = unionPathArray {
                
        array.forEach(unionPath.append)
                
        path0.append(unionPath.reversing())
        let layerUnion = CAShapeLayer()
        layerUnion.path = path0.cgPath
                
        blurView.layer.mask = layerUnion
    }
            
    

    Output:

    enter image description here

    EDIT

    It appears that this method doesn’t work properly when using UIBezierPath(roundedRect:cornerRadius:). To overcome that, here is how we can construct our own func to do that:

    
    extension UIBezierPath {
        
        convenience init(rectangleIn rect: CGRect, cornerRadius: CGFloat) {
            self.init()
            
            move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
            addArc(withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius), radius: cornerRadius, startAngle: .pi, endAngle: 3.0 * .pi / 2.0, clockwise: true)
            addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
            addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY + cornerRadius), radius: cornerRadius, startAngle: 3.0 * .pi / 2.0, endAngle: 2 * .pi, clockwise: true)
            
            addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
            addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: true)
            addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY))
            addArc(withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - cornerRadius), radius: cornerRadius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: true)
            //addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
            
            close()
        }
    }
    
    

    We can also extend the above-mentioned solution to multiple paths. Here is one way to create the union of multiple paths:

    
    extension UIBezierPath {
        
        class func getUnion(of paths: [UIBezierPath]) -> UIBezierPath {
            var result = UIBezierPath()
            paths.forEach { subPath in
                guard let union = result.union(with: subPath) else { return }
                let unionCombined = UIBezierPath()
                union.forEach(unionCombined.append)
                result = unionCombined
            }
            return result
        }
      
    }
    
    

    Here is an example:

    
        let rect1 = CGRect(x: 100, y: 100, width: 200, height: 180)
        let rect2 = CGRect(x: 150, y: 200, width: 200, height: 200)
        let rect3 = CGRect(x: 150, y: 500, width: 100, height: 100)
        let rect4 = CGRect(x: 150, y: 800, width: 300, height: 100)
            
        let pathBase = UIBezierPath(rect: blurView.bounds)
        let path1 = UIBezierPath(rectangleIn: rect1, cornerRadius: 20.0)
        let path2 = UIBezierPath(rect: rect2)
        let path3 = UIBezierPath(ovalIn: rect3)
        let path4 = UIBezierPath(ovalIn: rect4)
            
        let union = UIBezierPath.getUnion(of: [path1, path2, path3, path4])
        pathBase.append(union.reversing())
        let layerUnion = CAShapeLayer()
        layerUnion.path = pathBase.cgPath
            
        blurView.layer.mask = layerUnion
            
            
    

    And the output:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search