I'm trying to add a golden sheen on an UIImageView
(variable named assetImage
) to signify that it is something you can click on. This is the code that I'm using:
let shimmeringGradientLayer = CAGradientLayer()
shimmeringGradientLayer.frame = assetImage.bounds
shimmeringGradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
shimmeringGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
shimmeringGradientLayer.colors = [
Constants.Shimmering.gradientColorOne,
Constants.Shimmering.gradientColorTwo,
Constants.Shimmering.gradientColorOne
]
shimmeringGradientLayer.locations = [0.0, 0.5, 1.0]
/* Adding the gradient layer on to the view */
assetImage.layer.addSublayer(shimmeringGradientLayer)
let animation = CABasicAnimation(keyPath: "shimmer")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = .infinity
animation.duration = 0.9
shimmeringGradientLayer.add(animation, forKey: animation.keyPath)
However, since the image that I'm using has alpha channels, the result looks like this:
But what I want is something more like this, where the gradient only shows on the parts of the UIImageView
that has a non-transparent alpha. Note that this image isn't entirely clipped to the bounds of the image -- I'm not the best at using Figma!
Is this possible? I've tried adding the gradient layer as a mask of the assetImage
instead of adding it as a sublayer (using the code below), but the gradient is no longer visible.
assetImage.layer.mask = shimmeringGradientLayer
We can do this by using the same image as a mask for the gradient layer.
I'll use this image (kinda, sorta, similar to yours):
and a clouds image for the "background" image.
Looks like this:
Nothing special... that's exactly what we expect.
Now, we'll add a gradient layer on the "cabinet" image view:
Again, just what we expect, but not what we want.
So, we set the .opacity
of the gradient layer to 0.5
:
Still not what we want, and you've already done all of that.
The "tricky" part comes next. We'll have subclassed UIImageView
where we've added the gradient layer. Then:
// new CALayer
let cl = CALayer()
cl.frame = bounds
// set its contents to the image
cl.contents = image?.cgImage
// use it to mask the gradient layer
shimmeringGradientLayer.mask = cl
and it looks like this:
Now we can change the gradient colors and animate them to get a "golden sheen":
Here's a sample custom class:
class GradientMaskedImageView: UIImageView {
// so we can toggle on/off
public var isShimmering: Bool = false {
didSet {
doShimmer()
}
}
// gradient colors: adjust as desired, or set from controller class
public var gradColors: [UIColor] = [
.yellow.withAlphaComponent(0.25),
.yellow,
.yellow.withAlphaComponent(0.25),
] {
didSet {
shimmeringGradientLayer.colors = gradColors.map({ $0.cgColor })
}
}
private let shimmeringGradientLayer = CAGradientLayer()
convenience init() {
self.init(frame: .zero)
}
override init(image: UIImage?) {
super.init(image: image)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
shimmeringGradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
shimmeringGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
shimmeringGradientLayer.colors = gradColors.map({ $0.cgColor })
shimmeringGradientLayer.locations = [0.0, 0.5, 1.0]
/* Adding the gradient layer on to the view */
layer.addSublayer(shimmeringGradientLayer)
}
override func layoutSubviews() {
shimmeringGradientLayer.frame = bounds
// we want to start with the gradient layer opacity at Zero
shimmeringGradientLayer.opacity = 0.0
}
public func doShimmer() {
if !isShimmering {
// stop the animation and "hide" the gradient layer
shimmeringGradientLayer.removeAllAnimations()
shimmeringGradientLayer.opacity = 0.0
} else {
// we only need to setup the mask once
if shimmeringGradientLayer.mask == nil {
// new CALayer
let cl = CALayer()
cl.frame = bounds
// set its contents to the image
cl.contents = image?.cgImage
// use it to mask the gradient layer
shimmeringGradientLayer.mask = cl
}
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = .infinity
animation.duration = 0.9
shimmeringGradientLayer.add(animation, forKey: animation.keyPath)
// adjust as desired
shimmeringGradientLayer.opacity = 0.5
}
}
}
and an example controller:
class ShimmerTestVC: UIViewController {
let bkgImageView = UIImageView()
let testView = GradientMaskedImageView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
guard let img = UIImage(named: "cabinet"),
let bkImg = UIImage(named: "clouds")
else {
return
}
testView.image = img
bkgImageView.image = bkImg
bkgImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bkgImageView)
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
bkgImageView.widthAnchor.constraint(equalToConstant: 300.0),
bkgImageView.heightAnchor.constraint(equalTo: bkgImageView.widthAnchor, multiplier: 1.0),
bkgImageView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
bkgImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// we want the custom view to have the same aspect ratio
// as the image we're using
testView.widthAnchor.constraint(equalToConstant: 200.0),
testView.heightAnchor.constraint(equalTo: testView.widthAnchor, multiplier: img.size.height / img.size.width),
testView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testView.isShimmering.toggle()
}
}
Tapping anywhere will toggle the "shimmer" on/off.