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?
1 Answers
Not exactly clear what you mean by "intersection of the two shapes" ... but maybe this is what you're going for:
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:
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 ovalCAShapeLayer
- a NON-filled gray-bordered oval
CAShapeLayer
So, we start with a view:
add a white-filled gray-bordered rectangle CAShapeLayer
:
add a white-filled NON-bordered oval CAShapeLayer
(red first, to show it clearly):
add a CAGradientLayer
:
mask it with an oval CAShapeLayer
:
finally, add a NON-filled gray-bordered oval CAShapeLayer
:
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
}
}

- 69,424
- 5
- 50
- 86
-
This doesn't address the question. You considered the box a UIView, whereas it is a CAShapeLayer (as well as the circle). Also, I don't want to get rid of the empty part in the circle (see the image I posted). The thing is that the gradient should only be applied to the intersection (common area) between the box and the circle, but you can still see the full circle perimeter. – Asteroid Apr 19 '22 at 21:04
-
@Asteroid - OK, to try and clarify... An outlined circle and an outlined rectangle (except for the part where the rect crosses the circle)... and that should be used as a **mask**? Should the white areas be opaque-white? Or transparent? Is the outlined rect supposed to be the same size as the gradient rect? Or, are you just trying to show an "assembled mask" that you want to use at any size on any other-sized view? Or... is the image on the right your "goal" image? – DonMag Apr 19 '22 at 23:33
-
The image on the right is the goal image. As you can see only the intersection of the two shapes is masking the gradient layer. – Asteroid Apr 20 '22 at 03:21
-
@Asteroid ... it would be very complex - possibly impossible - to do this with a single mask. I added an **Edit** to my answer -- see if that gets you closer to your goal. – DonMag Apr 20 '22 at 16:00