Understanding UIAppearance Container Hierarchies

I recently had my first run-in with the UIAppearance infrastructure that has been built into iOS for, admittedly, a long time. Given my experience with CSS, I brought along some assumptions of how appearance(whenContainedInInstancesOf:) would probably work. Naturally, these assumptions were very wrong.

Fortunately, the documentation is both precise and concise in its description of how it works:

In any given view hierarchy, the outermost appearance proxy wins. Specificity (depth of the chain) is the tie-breaker. In other words, the containment statement in appearanceWhenContainedIn: is treated as a partial ordering. Given a concrete ordering (actual subview hierarchy), UIKit selects the partial ordering that is the first unique match when reading the actual hierarchy from the window down.

Does that clear things up? If so, feel free to stop reading. Otherwise, here are some lessons I learned:

Gotcha 1: The Order of Containers

In CSS, we read a hierarchy from left to right. For example:

.main-column .box-container button {
  /* this matches button tags that appear inside a tag with the “box-container” class that itself appears inside of a tag with the “main-column” class */
  color: red;

This is, however, the opposite of how it’s done with UIAppearance. An analogous example would look like:

UIButton.appearance(whenContainedInInstancesOf: [BoxContainer.self, MainViewController.self]).tintColor = UIColor.red;

As you can see, outermost parents appear toward the end of the array provided to the appearance proxy. I presume this is because it maintains a consistent ordering when you also consider the UIButton class for which you’re accessing the appearance proxy.

Gotcha 2: Preference Is Given to Matches of the Outermost Parent

Let’s imagine that we have a UILabel inside a CustomView inside a ViewController, and that we also have another UILabel outside (next to) the CustomView. We want to say that all labels inside the ViewController should be gray, and also that all labels inside of the CustomView should be red. We write this:

UILabel.appearance(whenContainedInInstancesOf: [CustomView.self]).textColor = red;
UILabel.appearance(whenContainedInInstancesOf: [ViewController.self]).textColor = UIColor.gray;

What we see is that all the labels are actually gray. What gives?

This is where the last sentence from the documentation above comes into play: “UIKit selects the partial ordering that is the first unique match when reading the actual hierarchy from the window down.”

Effectively, UIKit first looks at the ViewController. UIKit finds that the UILabel does exist inside the ViewController, so that rule wins. The other rule involving CustomView is never even considered. Fortunately, in this case, we are able to influence UIKit by providing a more specific hierarchy, as longer matches win over shorter matches.

UILabel.appearance(whenContainedInInstancesOf: [CustomView.self, ViewController.self]).textColor = red;
UILabel.appearance(whenContainedInInstancesOf: [ViewController.self]).textColor = UIColor.gray;

Wrapping Up

There’s really not much more to it than this. It took me a bit of trial and error to internalize the rules, but now, I think I can summarize them succinctly:

  • Read the hierarchy right-to-left.
  • Matches higher up in the view hierarchy (i.e., closer to the UIWindow) win over views down the hierarchy.
  • Longer rules win over shorter rules.

Hope that helps!