skip to Main Content

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


  1. Solution: Set the color or state of the label to your desired animation start value in viewWillAppear. Then trigger the animation from within viewDidAppear.

    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.

    Login or Signup to reply.
  2. 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:

    class ViewController: UIViewController {
    
        var myButton: MyButton = MyButton()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.addSubview(myButton)
            myButton.frame.origin.x = 200
            myButton.frame.origin.y = 200
            myButton.addAction(.init { [weak self] _ in
                guard let self = self else { return }
                self.myButton.isSelected = true
                self.navigationController?.pushViewController(ViewController2(), animated: true)
            }, for: .touchUpInside)
        }
        
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            guard self.myButton.isSelected else { return }
            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)
        }
        
        override func didMoveToSuperview() {
            super.didMoveToSuperview()
            // even though this is being called AFTER
            //  we have moved to the superview
            //  we should safely unwrap it
            guard let sv = self.superview else { return }
            // if we haven't added the label yet
            if label.superview == nil {
                // add the label as a sibling to self
                label.translatesAutoresizingMaskIntoConstraints = false
                sv.addSubview(label)
                NSLayoutConstraint.activate([
                    label.topAnchor.constraint(equalTo: self.topAnchor, constant: 40.0),
                    label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 40.0),
                ])
            }
        }
    }
    
    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
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search