Search code examples
iosswiftuikit

iOS label is invisible during viewWillAppear, backing from a pushed view controller


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.


Solution

  • 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
        }
    }