I have created a subclass for UIButton where I am drawing a CAShapelayer to serve as circular background color for the button, which works just fine. However that CAShapeLayer is now covering the button image.
What's the best way to handle this? I have tried inserting the CAShapeLayer but that didn't have any effect. Is there a way to bring the button image to the foreground?
This is how I am currently casting the button image:
var buttonImage: UIImage = UIImage(named: "icon_check")! {
didSet {
setImage(buttonImage, for: .normal)
}
}
Update: the code for the entire class:
class PulseButton: UIButton {
let buttonFillLayer = CAShapeLayer()
let pulsatingLayer = CAShapeLayer()
var buttonFillLayerFillColor: UIColor = .buttonLayerFillColor { didSet { buttonFillLayer.fillColor = buttonFillLayerFillColor.cgColor } }
var pulsatingLayerFillColor: UIColor = .pulsatingLayerFillColor { didSet { pulsatingLayer.fillColor = pulsatingLayerFillColor.cgColor } }
var buttonFillLayerStrokeStart: CGFloat = 0 { didSet { buttonFillLayer.strokeStart = buttonFillLayerStrokeStart } }
var pulsatingLayerStrokeStart: CGFloat = 0 { didSet { pulsatingLayer.strokeStart = pulsatingLayerStrokeStart } }
var buttonFillLayerStrokeEnd: CGFloat = 1 { didSet { buttonFillLayer.strokeEnd = buttonFillLayerStrokeEnd } }
var pulsatingLayerStrokeEnd: CGFloat = 1 { didSet { pulsatingLayer.strokeEnd = pulsatingLayerStrokeEnd } }
var buttonImage: UIImage = UIImage(named: "icon_check")! {
didSet {
setImage(buttonImage, for: .normal)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
}
private func setupLayout() {
pulsatingLayer.strokeColor = UIColor.clear.cgColor
pulsatingLayer.fillColor = buttonFillLayerFillColor.cgColor
pulsatingLayer.strokeStart = buttonFillLayerStrokeStart
pulsatingLayer.strokeEnd = buttonFillLayerStrokeEnd
buttonFillLayer.strokeColor = UIColor.clear.cgColor
buttonFillLayer.fillColor = pulsatingLayerFillColor.cgColor
buttonFillLayer.strokeStart = pulsatingLayerStrokeStart
buttonFillLayer.strokeEnd = pulsatingLayerStrokeEnd
layer.addSublayer(pulsatingLayer)
layer.addSublayer(buttonFillLayer)
}
private func updatePaths() {
print("Updating")
//Parameters for layers
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = (min(bounds.width, bounds.height)) / 2
let path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true).cgPath
pulsatingLayer.transform = CATransform3DIdentity
pulsatingLayer.frame = bounds
pulsatingLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
buttonFillLayer.transform = CATransform3DIdentity
buttonFillLayer.frame = bounds
buttonFillLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
pulsatingLayer.path = path
buttonFillLayer.path = path
}
func animateCircle() {
let scaleUpAimation = CABasicAnimation(keyPath: "transform.scale")
scaleUpAimation.fromValue = 1
scaleUpAimation.toValue = 1.4
scaleUpAimation.duration = 0.8
scaleUpAimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
self.pulsatingLayer.add(scaleUpAimation, forKey: "pulsing")
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0.6
opacityAnimation.toValue = 0
opacityAnimation.duration = 0.8
opacityAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
pulsatingLayer.add(opacityAnimation, forKey: "changing opacity")
}
}
You have two problems:
You're not actually setting the button's image. You try to by creating a didSet
observer for buttonImage
, but that observer doesn't get called at initialization time. It only gets called later if buttonImage
is reassigned. One way to fix this problem is by setting it to itself in setupLayout()
, like this:
private func setupLayout() {
buttonImage = { buttonImage }()
Note that Swift won't let you assign a property to itself, so I use an anonymous closure to get the current buttonImage
value.
You need to let the button do its normal layout before you add your own layers if you want to be sure that your layers go under its other layers. So do not add the layers in setupLayout
. Wait until layoutSubviews
to insert the layers:
override func layoutSubviews() {
super.layoutSubviews()
if pulsatingLayer.superlayer == nil {
layer.insertSublayer(pulsatingLayer, at: 0)
layer.insertSublayer(buttonFillLayer, at: 1)
}
updatePaths()
}
Result: