3

I setup UILabel appearance in my app delegate using:

UILabel.appearance().textColor = UIColor.white

I also have a custom UIView subclass that contains a UILabel along with some other elements (omitted here):

@IBDesignable
class CustomView: UIView {
    private let descriptionLabel = HCLabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        self.setup()
    }

    private func setup() {
        self.descriptionLabel.textColor = UIColor.black

        // ... other things not related to descriptionLabel
    }
}

If I instantiate CustomView in a storyboard, everything works just fine. If, however, I instantiate it in code, the descriptionLabel is white (appearance color), not black (the color I set). What's going on here? The way I understood it was that if I set a custom color, the appearance color will not be used.

BlackWolf
  • 5,239
  • 5
  • 33
  • 60
  • Have you verified that `setup` is actually called? – rmaddy Nov 30 '18 at 17:58
  • yes it is called and the color of UILabel is changed, but apparently gets changed back somewhere later – BlackWolf Nov 30 '18 at 18:02
  • I edited the question to fix an error - `CustomView` is not instantiated using a storyboard, but programmatically. – BlackWolf Nov 30 '18 at 18:49
  • Implement `didMoveToSuperview` and call setup there. – matt Nov 30 '18 at 18:58
  • This does indeed work, but can you explain why? I know UIAppearance properties are set when a view moves to superview/window, but I was also under the impression that setting a custom value would prevent this. I don't think if another dev on the project will use UILabel in a similar way 6 months from now, he/she will ever be able to figure out why setting textColor simply has no effect... – BlackWolf Nov 30 '18 at 19:07
  • I think it's pretty obvious, but maybe that's just me. I gave more detail in my answer below. – matt Nov 30 '18 at 21:31

1 Answers1

5

What you're experiencing is simply a matter of the exact timing with which the UIAppearance proxy applies its settings to a new UIView. When are we to suppose it does this? It can't possibly do it before init, because init is the first thing that happens in the life of the UIView. Thus, the order of events is like this:

override init(frame: CGRect) {
    super.init(frame: frame)
    setup() // black
}
// and some time later, UIAppearance proxy comes along and sets it to white

So your goal is to call setup pretty early in the life of the label — and certainly before the user ever has a chance to see it — but not so early that the UIAppearance proxy acts later. Let's move the call to setup to a bit later in the life of the label:

// some time earlier, UIAppearance proxy sets it to white
override func didMoveToSuperview() {
    setup() // black
}

Now we're acting after the appearance proxy has had a chance to act, and so your settings are the last to operate, and they win the day.

We remain in ignorance of how early we could move the call to setup and still come along after the appearance proxy setting has been obeyed. If you have time, you might like to experiment with that. For example, willMoveToSuperview is earlier; if you call setup there (and not in didMoveToSuperview), does that work? Play around and find out!

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thank you for the response. I still think I don't quite understand how UIAppearance is supposed to be used, though - if using it means I can't customize UILabel's anymore in interface builder, and in code only by jumping through hoops every time I use it, this seems like a pretty flawed design to me. I guess I will not be able to use this then. Still, your answer works and is well-explained, so I will accept it ;-) – BlackWolf Dec 01 '18 at 08:50