Search code examples
iosswiftuibuttonsubclasscashapelayer

Possible to set UIButton image to be at zero index?


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")
}
}

Solution

  • You have two problems:

    1. 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.

    2. 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:

    demo