Yes, you can animate background. The code above works fine as long as I don't override draw(rect). This was inspired by Apple's WWDC18 talk on "Designing Fluid Interfaces". The example video shows the animation on Apple's iPhone calculator button.
import UIKit
typealias ButtonColor = (foreground:UIColor, background:UIColor)
protocol CalculatorButtonAttributes {
var borderWidth: CGFloat { get }
var borderColor: UIColor { get }
var spacing: (dx:CGFloat,dy:CGFloat) { get }
func cornerRound( for size: CGSize) -> CGFloat
var digitColor: ButtonColor { get }
var equalColor: ButtonColor { get }
var functionColor: ButtonColor { get }
var editColor: ButtonColor { get }
var operatorColor: ButtonColor { get }
var highlightColor: ButtonColor { get }
}
protocol CalculatorButtonTitleAttributes {
func range(for tag:Int) -> NSRange?
func attributedTitle(with pointSize:CGFloat, for tag:Int, and foregroundColor: UIColor) -> NSAttributedString
var animationTime: TimeInterval { get }
var scriptFontScale: CGFloat { get }
func pointSize( for tag:Int) -> CGFloat
}
typealias CalculatorButtonData = CalculatorButtonAttributes & CalculatorButtonTitleAttributes
@objcMembers public class CalculatorButton: UIButton {
static var buttonDataDelegate: CalculatorButtonData!
private var animator = UIViewPropertyAnimator()
internal var delegate: CalculatorButtonData {
guard let delegate = CalculatorButton.buttonDataDelegate else {
fatalError("CalculatorButtonData delegate hasn't been defined. Use initDelegate()")
}
return delegate
}
@objc class func initDelegate(_ delegate:Any) {
CalculatorButton.buttonDataDelegate = delegate as? CalculatorButtonData
}
public override init(frame: CGRect) {
super.init(frame: frame)
sharedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
sharedInit()
}
private func sharedInit() {
layer.masksToBounds = true
addTarget(self, action: #selector(touchDown), for: [.touchDown, .touchDragEnter])
addTarget(self, action: #selector(touchUp), for: [.touchUpInside, .touchDragExit, .touchCancel])
dPrint("sharedInit: \(description)")
}
@objc private func touchDown() {
animator.stopAnimation(true)
backgroundColor = delegate.highlightColor.background
setTitleColor(delegate.highlightColor.foreground, for: .normal)
setTitleColor(delegate.highlightColor.foreground, for: .highlighted)
dPrint("animator stop: \(description)")
}
@objc private func touchUp() {
animator = UIViewPropertyAnimator(duration: 1.5, curve: .easeOut, animations: {
self.backgroundColor = self.buttonColor.background
self.setTitleColor(self.buttonColor.foreground, for: .normal)
dPrint("animator start: \(self.description)")
})
animator.startAnimation()
}
/// This method applies general theme properties to buttons. The properties are provided via the CalculatorButtonData delegate.
///
/// *Note*: The source frame is inset and cornerRadius applied; so, typically, this should be called with an initial frame; as oppose
/// to one that has already been inset.
func applyThemeProperties() {
layer.borderWidth = delegate.borderWidth
layer.borderColor = delegate.borderColor.cgColor
layer.borderColor = delegate.borderColor.cgColor // [appTheme.buttonBorderColor CGColor];
layer.borderWidth = delegate.borderWidth
// frame = frame.insetBy(dx: appTheme.buttonInset.left + appTheme.buttonInset.right, dy: appTheme.buttonInset.top + appTheme.buttonInset.bottom)
frame = frame.insetBy(dx: delegate.spacing.dx, dy: delegate.spacing.dy)
layer.cornerRadius = delegate.cornerRound(for: frame.size)
backgroundColor = buttonColor.background
titleLabel?.textColor = buttonColor.foreground
}
public override var tag: Int {
get {
return super.tag
}
set {
super.tag = newValue
refresh()
}
}
func refresh() {
superscriptRange = delegate.range(for: tag)
setTitleColor(buttonColor.foreground, for: .normal)
setTitleColor(buttonColor.foreground, for: .highlighted)
// setTitleColor(delegate.highlightColor.foreground, for: .highlighted)
backgroundColor = buttonColor.background
applyThemeProperties()
}
}
extension CalculatorButton {
open override var description: String {
let buttonTitle = title(for: .normal) ?? "undefined"
let accLabel = accessibilityLabel ?? "undefined"
return "CalculatorButton: " + "Tag: \(tag) Title: '\(buttonTitle)' accLabel '\(accLabel)'"
}
}
Its all somewhat experimental so documentation is a practically nothing. Most of the properties come from delegates which makes theming a bit easier; of course, you can substitue hard-coded values if you intend to run the code. All buttons are created in IB. If I subclass CalculatorButton and add even an empty draw(rect) my touch event targets get called but the animator does nothing.
I've seen other implimentations use a UIControl with a UILabel for button text. It would be slighly easier for me to stay with a UIButton. But the draw(rect) issue may also exist with UIControl implementation.