iosswiftuiviewuibezierpathcashapelayer

How to use UIBezierPath() on CAShapeLayer() multiple times for same view in Swift 5


I want to cut off the upper and lower portion of a container view in my table view cell using UIBezierPath() & CAShapeLayer(). The code is as follows:

func cutView() {
        let containerViewHeight: CGFloat = containerView.frame.height
        let headerCut: CGFloat = 50
        let newHeight = containerViewHeight - headerCut/2
        let newHeaderUpperLayer = CAShapeLayer()
        let newHeaderLowerLayer = CAShapeLayer()
        
        newHeaderUpperLayer.fillColor = UIColor.black.cgColor
        newHeaderLowerLayer.fillColor = UIColor.black.cgColor
        containerView.layer.mask = newHeaderUpperLayer
        containerView.layer.mask = newHeaderLowerLayer
        
        let getViewFrame = CGRect(x: 0, y: -newHeight, width: containerView.bounds.width, height: containerViewHeight)
        let cutDirectionUpper = UIBezierPath()
        let cutDirectionLower = UIBezierPath()
        
        cutDirectionUpper.move(to: CGPoint(x: 0, y: 0))
        cutDirectionUpper.addLine(to: CGPoint(x: getViewFrame.width, y: headerCut))
        cutDirectionUpper.addLine(to: CGPoint(x: getViewFrame.width, y: getViewFrame.height))
        cutDirectionUpper.addLine(to: CGPoint(x: 0, y: getViewFrame.height))
        
        cutDirectionLower.move(to: CGPoint(x: 0, y: 0))
        cutDirectionLower.addLine(to: CGPoint(x: getViewFrame.width, y: 0))
        cutDirectionLower.addLine(to: CGPoint(x: getViewFrame.width, y: getViewFrame.height))
        cutDirectionLower.addLine(to: CGPoint(x: 0, y: getViewFrame.height - headerCut))
        
        newHeaderUpperLayer.path = cutDirectionUpper.cgPath
        newHeaderLowerLayer.path = cutDirectionLower.cgPath
    }

It's working separately but not together. What I am missing here?

If I separately run it, it works like this:

enter image description here enter image description here

But what I want is this:

enter image description here

Using combined UIBezierPath():

func cutView() {
        let containerViewHeight: CGFloat = containerView.frame.height
        let headerCut: CGFloat = 50
        let newHeight = containerViewHeight - headerCut/2
        let newHeaderLayer = CAShapeLayer()
        
        newHeaderLayer.fillColor = UIColor.black.cgColor
        containerView.layer.mask = newHeaderLayer
        
        let getViewFrame = CGRect(x: 0, y: -newHeight, width: containerView.bounds.width, height: containerViewHeight)
        let cutDirectionLower = UIBezierPath()
        let cutDirectionUpper = UIBezierPath()
        
        cutDirectionUpper.move(to: CGPoint(x: 0, y: 0))
        cutDirectionUpper.addLine(to: CGPoint(x: getViewFrame.width, y: headerCut))
        cutDirectionUpper.addLine(to: CGPoint(x: getViewFrame.width, y: getViewFrame.height))
        cutDirectionUpper.addLine(to: CGPoint(x: 0, y: getViewFrame.height))
        
        cutDirectionLower.move(to: CGPoint(x: 0, y: 0))
        cutDirectionLower.addLine(to: CGPoint(x: getViewFrame.width, y: 0))
        cutDirectionLower.addLine(to: CGPoint(x: getViewFrame.width, y: getViewFrame.height))
        cutDirectionLower.addLine(to: CGPoint(x: 0, y: getViewFrame.height - headerCut))
        
        cutDirectionUpper.append(cutDirectionLower)
        newHeaderLayer.path = cutDirectionUpper.cgPath
    }

Using combined CGMutablePath():

func cutView() {
        let containerViewHeight: CGFloat = containerView.frame.height
        let headerCut: CGFloat = 50
        let newHeight = containerViewHeight - headerCut/2
        let newHeaderLayer = CAShapeLayer()
        let combinedPath = CGMutablePath()
        
        newHeaderLayer.fillColor = UIColor.black.cgColor
        containerView.layer.mask = newHeaderLayer
        
        let getViewFrame = CGRect(x: 0, y: -newHeight, width: containerView.bounds.width, height: containerViewHeight)
        let cutDirectionLower = UIBezierPath()
        let cutDirectionUpper = UIBezierPath()
        
        cutDirectionUpper.move(to: CGPoint(x: 0, y: 0))
        cutDirectionUpper.addLine(to: CGPoint(x: getViewFrame.width, y: headerCut))
        cutDirectionUpper.addLine(to: CGPoint(x: getViewFrame.width, y: getViewFrame.height))
        cutDirectionUpper.addLine(to: CGPoint(x: 0, y: getViewFrame.height))
        
        cutDirectionLower.move(to: CGPoint(x: 0, y: 0))
        cutDirectionLower.addLine(to: CGPoint(x: getViewFrame.width, y: 0))
        cutDirectionLower.addLine(to: CGPoint(x: getViewFrame.width, y: getViewFrame.height))
        cutDirectionLower.addLine(to: CGPoint(x: 0, y: getViewFrame.height - headerCut))
        
        combinedPath.addPath(cutDirectionUpper.cgPath)
        combinedPath.addPath(cutDirectionLower.cgPath)
        
        newHeaderLayer.path = combinedPath
    }

Both of them remove the upper and lower cut. What I am missing here?
Output of using combined CGMutablePath() & combined UIBezierPath() separately is as follows:

enter image description here


Solution

  • I think you'll find it much easier to work with a subclassed UIImageView that handles the masking for you.

    Start with defining your shape like this:

    enter image description here

    You will move to pt1, addLine to pt2, addLine to pt3, addLine to pt4, and then close the path.

    To get that to work automatically, update the path in our subclass in layoutSubviews() -- that way it will adjust its size whenever the view changes size.

    Here is an example I used to create that image:

    class CutImageView: UIImageView {
    
        let maskLayer = CAShapeLayer()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
    
        func commonInit() -> Void {
            layer.mask = maskLayer
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            let headerCut: CGFloat = 50
    
            let pt1: CGPoint = CGPoint(x: bounds.minX, y: bounds.maxY - headerCut)
            let pt2: CGPoint = CGPoint(x: bounds.minX, y: bounds.minY)
            let pt3: CGPoint = CGPoint(x: bounds.maxX, y: headerCut)
            let pt4: CGPoint = CGPoint(x: bounds.maxX, y: bounds.maxY)
    
            let pth = UIBezierPath()
    
            pth.move(to: pt1)
            pth.addLine(to: pt2)
            pth.addLine(to: pt3)
            pth.addLine(to: pt4)
            pth.close()
    
            maskLayer.path = pth.cgPath
    
        }
    
    }
    
    class ViewController: UIViewController {
    
        let cutImageView: CutImageView = {
            let v = CutImageView(frame: CGRect.zero)
            v.translatesAutoresizingMaskIntoConstraints = false
            v.contentMode = .scaleToFill
            return v
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            guard let bkgImage = UIImage(named: "background") else {
                fatalError("missing images")
            }
    
            view.backgroundColor = .systemGreen
    
            view.addSubview(cutImageView)
    
            let g = view.safeAreaLayoutGuide
    
            NSLayoutConstraint.activate([
    
                cutImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
                cutImageView.centerXAnchor.constraint(equalTo: g.centerXAnchor, constant: 0.0),
                cutImageView.widthAnchor.constraint(equalToConstant: 300.0),
                cutImageView.heightAnchor.constraint(equalToConstant: 240.0),
    
    
            ])
    
            cutImageView.image = bkgImage
    
        }
    }