I have a button with 2 icons and a label. The button has a different background color for .selected state. When I click the button, the button is selected and a new ViewController is pushed. I’m trying to mimic the behaviour of UITableViewCell.setSelected(animated:), so when the user backs out from the new ViewController I want the background color to animate. I use this:
UIView.transition(with: button, duration: 0.3, options: .transitionCrossDissolve) { button.isSelected = false }
It works fine when I put it in viewDidAppear but it’s a bit too late so I want it in viewWillAppear. Problem is when I do it from viewDidAppear, the label on my button starts as transparent. The icons are fine and static but the label will animate from transparent to its color. Why? How I can prevent this?
Here is a small demo:
import UIKit
class ViewController: UIViewController {
var myButton: MyButton?
override func viewDidLoad() {
super.viewDidLoad()
myButton = MyButton()
view.addSubview(myButton!)
myButton?.frame.origin.x = 200
myButton?.frame.origin.y = 200
myButton?.addAction(.init { [weak self] _ in
self?.myButton?.isSelected = true
self?.navigationController?.pushViewController(ViewController2(), animated: true)
}, for: .touchUpInside)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIView.transition(with: myButton!, duration: 3, options: .transitionCrossDissolve) { self.myButton!.isSelected = false }
}
// override func viewDidAppear(_ animated: Bool) {
// super.viewDidAppear(animated)
// UIView.transition(with: myButton!, duration: 3, options: .transitionCrossDissolve) { self.myButton!.isSelected = false }
//
// }
}
class ViewController2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
}
}
class MyButton: UIButton {
let label = UILabel("Hahahaha", font: .systemFont(ofSize: 14), textColor: .black)
convenience init() {
self.init(frame: .init(x: 0, y: 0, width: 200, height: 200))
setBackgroundColor(color: .white, forState: .normal)
setBackgroundColor(color: .gray, forState: .highlighted)
setBackgroundColor(color: .gray, forState: .selected)
addSubview(label)
label.frame.origin = .init(x: 40, y: 40)
}
}
extension UIButton {
func setBackgroundColor(color: UIColor, forState: UIControl.State) {
self.clipsToBounds = true
UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
if let context = UIGraphicsGetCurrentContext() {
context.setFillColor(color.cgColor)
context.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
let colorImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.setBackgroundImage(colorImage, for: forState)
}
}
}
extension UILabel {
convenience init(_ text: String = "", font: UIFont? = nil, textColor: UIColor? = .black) {
self.init()
self.text = text
self.font = font
sizeToFit()
self.textColor = textColor
}
}
Try backing out from ViewController2 , see label starts invisible. Now comment out the viewWillAppear and use viewDidAppear instead, the label starts black.
2
Answers
Solution: Set the color or state of the label to your desired animation start value in
viewWillAppear
. Then trigger the animation from withinviewDidAppear
.Reason:
When the button is pressed and you initiate a navigation, the button is not highlighted anymore and neither is it selected anymore. Thus it returns to it’s normal state.
Now you navigate back.
viewWillAppear
is called. It’s name implies "the view will be shown in a moment, but it’s not visible yet". So when you start an animation at this point, UIKit assume that even if it did animate something, it won’t be visible to the user. (The view is not yet visible = animations on this view won’t be seen either.) To save resources UIKit will ignore the animation.So the solution is: set the button state early and then start the animation once
viewDidAppear
is called and UIKit will actually execute it.Here is one approach…
Stick with your
UIButton
subclass, but instead of adding the label as a subview of the button, add it as a sibling view — meaning, it will be a subview of the same view of which the button is a subview.We’ll add that label to the view hierarchy in the button’s
didMoveToSuperview()
function, and we’ll use auto-layout to constrain it at(40, 40)
relative to the top-left corner of the button – checking to make sure we only do so once.Most of this code is your original code: