Asteroid
Asteroid

Reputation: 1118

Mask a gradient layer with the intersection of two shape layers in Swift

I have two shapes of type CAShapeLayer (e.g. one box and a circle) and a gradient layer of type CAGradientLayer. How can I mask the gradient layer with the intersection of the two shapes like this picture in Swift?

enter image description here

Upvotes: 1

Views: 792

Answers (1)

DonMag
DonMag

Reputation: 77477

Not exactly clear what you mean by "intersection of the two shapes" ... but maybe this is what you're going for:

enter image description here

To get that, we can create a CAShapeLayer with an oval (round) path, and use it as a mask on the gradient layer.

Here's some example code:

class GradientMaskingViewController: UIViewController {

    let gradView = MaskedGradView()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        gradView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(gradView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            gradView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
            gradView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
            gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor),
            gradView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        ])
        
        gradView.colorArray = [
            .blue, .orange, .purple, .yellow
        ]

    }
    
}

class MaskedGradView: UIView {
    
    enum Direction {
        case horizontal, vertical, diagnal
    }
    public var colorArray: [UIColor] = [] {
        didSet {
            setNeedsLayout()
        }
    }
    public var locationsArray: [NSNumber] = [] {
        didSet {
            setNeedsLayout()
        }
    }
    public var direction: Direction = .vertical {
        didSet {
            setNeedsLayout()
        }
    }
    
    private let gLayer = CAGradientLayer()
    private let maskLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        // add gradient layer as a sublayer
        layer.addSublayer(gLayer)
        
        // mask it
        gLayer.mask = maskLayer

        // we'll use a 120-point diameter circle for the mask
        maskLayer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)).cgPath
        
        // so we can see this view's frame
        layer.borderColor = UIColor.black.cgColor
        layer.borderWidth = 1
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // update gradient layer
        //  frame
        //  colors
        //  locations
        
        gLayer.frame = bounds
        
        gLayer.colors = colorArray.map({ $0.cgColor })
        
        if locationsArray.count > 0 {
            gLayer.locations = locationsArray
        }
        
        switch direction {
        case .horizontal:
            gLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
            gLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
        case .vertical:
            gLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
            gLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
        case .diagnal:
            gLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
            gLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
        }
        
    }

    // touch code to drag the circular mask around
    private var curPos: CGPoint = .zero
    private var lPos: CGPoint = .zero
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        curPos = touch.location(in: self)
        lPos = maskLayer.position
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let newPos = touch.location(in: self)
        let diffX = newPos.x - curPos.x
        let diffY = newPos.y - curPos.y
        
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        
        maskLayer.position = CGPoint(x: lPos.x + diffX, y: lPos.y + diffY)
        
        CATransaction.commit()  }
}

To help make it clear, I added touch handling code so you can drag the circle around inside the view:

enter image description here


Edit - after comment (but still missing details), let's try this again...

We can get the desired output by using multiple layers.

  • a white-filled gray-bordered rectangle CAShapeLayer
  • a white-filled NON-bordered oval CAShapeLayer
  • a CAGradientLayer masked with an oval CAShapeLayer
  • a NON-filled gray-bordered oval CAShapeLayer

So, we start with a view:

enter image description here

add a white-filled gray-bordered rectangle CAShapeLayer:

enter image description here

add a white-filled NON-bordered oval CAShapeLayer (red first, to show it clearly):

enter image description here enter image description here

add a CAGradientLayer:

enter image description here

mask it with an oval CAShapeLayer:

enter image description here enter image description here

finally, add a NON-filled gray-bordered oval CAShapeLayer:

enter image description here

Here's the example code:

class GradientMaskingViewController: UIViewController {
    
    let gradView = MultiLayeredGradView()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)

        gradView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(gradView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            gradView.widthAnchor.constraint(equalToConstant: 240.0),
            gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor),
            gradView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            gradView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        ])
        
    }
    
}

class MultiLayeredGradView: UIView {
    
    private let rectLayer = CAShapeLayer()
    private let filledCircleLayer = CAShapeLayer()
    private let gradLayer = CAGradientLayer()
    private let maskLayer = CAShapeLayer()
    private let outlineCircleLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        
        // add filled-bordered rect layer as a sublayer
        layer.addSublayer(rectLayer)
        
        // add filled circle layer as a sublayer
        layer.addSublayer(filledCircleLayer)
        
        // add gradient layer as a sublayer
        layer.addSublayer(gradLayer)
        
        // mask it
        gradLayer.mask = maskLayer
        
        // add outline circle layer as a sublayer
        layer.addSublayer(outlineCircleLayer)
        
        let bColor: CGColor = UIColor.gray.cgColor
        let fColor: CGColor = UIColor.white.cgColor
        
        // filled-outlined
        rectLayer.strokeColor = bColor
        rectLayer.fillColor = fColor
        rectLayer.lineWidth = 2
        
        // filled
        filledCircleLayer.fillColor = fColor
        
        // clear-outlined
        outlineCircleLayer.strokeColor = bColor
        outlineCircleLayer.fillColor = UIColor.clear.cgColor
        outlineCircleLayer.lineWidth = 2

        // gradient layer properties
        let colorArray: [UIColor] = [
            .blue, .orange, .purple, .yellow
        ]
        gradLayer.colors = colorArray.map({ $0.cgColor })
        gradLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // circle diameter is 45% of the width of the view
        let circleDiameter: CGFloat = bounds.width * 0.45
        
        // circle Top is at vertical midpoint
        // circle is moved Left by 25% of the circle diameter
        let circleBounds: CGRect = CGRect(x: bounds.minX - circleDiameter * 0.25,
                                          y: bounds.maxY * 0.5,
                                          width: circleDiameter,
                                          height: circleDiameter)
        
        // gradient layer fills the bounds
        gradLayer.frame = bounds
        
        let rectPath = UIBezierPath(rect: bounds).cgPath
        rectLayer.path = rectPath
        
        let circlePath = UIBezierPath(ovalIn: circleBounds).cgPath
        filledCircleLayer.path = circlePath
        outlineCircleLayer.path = circlePath
        maskLayer.path = circlePath
        
    }

}

Upvotes: 0

Related Questions