Misusing Appearance Selectors

UIAppearance is a pretty neat technology we can use to style our apps, but it turns out we can also use it for another tricky purpose.

Whether it’s right or wrong, I tend to approach app development as if I were still an Apple framework developer. Perhaps my background as a Objective-C developer is why I feel particularly comfortable extending UIKit classes with additional functionality. I’ll often find myself writing an extension of UIView where I’d like to know when the view has been added to the view hierarchy. I could just implement willMove(toSuperview:) (AKA -willMoveToSuperview:) of which the documentation says:

The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the superview changes.

But how can I be certain noone in our app or any library we use implements willMove(toSuperview:) or will ever implement willMove(toSuperview:)? Because they’re not expecting the implementation in UIView to do anything, they might not call super. Then my code won’t run.

Clearly, what I need are life-cycle hooks instead. I’m pretty certain I filed a radar about this while I was at Apple, but I’m not going to wait under water for them to be implemented.

Instead, lets consider UI_APPEARANCE_SELECTOR.

The documentation on UI_APPEARANCE_SELECTOR is nearly non-existent, but it marks an Objective-C selector as participating in the shadowy world of UIAppearance. UIKit evaluates appearance selectors on views as they are added to the view hierarchy. We can abuse this behaviour for our own nefarious ends.

I’m going to use Objective-C code here, because I can’t be bothered to figure this out in Swift. In my UIView categories, I include the following bogus UI_APPEARANCE_SELECTOR:

@interface UIView (Sample)

@property (nonatomic, setter=set_jw_bogus) BOOL jw_bogus UI_APPEARANCE_SELECTOR;

@end

Because we’re dealing with a category and the compiler can’t add properties to UIView, we’ll need to implement the getter & setter. But I don’t actually care about the value of jw_bogus. It’s purely there for the side effect of being called.

@implementation UIVIew (Sample)

- (void)set_jw_bogus:(BOOL)jw_bogus
{
    // Here's where we can trigger code that needs to run when the view is added to the view hierarchy.
}

- (BOOL)jw_bogus
{
    return YES;
}

@end

None of this will do any good unless we tell UIKit views should have a custom “appearance”. We can do that with a static constructor which will be executed as your app starts up. This grabs the UIAppearance proxy for UIView and sets the jw_bogus property to YES. This tells UIKit every instance of UIView should have it’s jw_bogus property set to YES after it is added to the view hierarchy.

__attribute__((constructor))
static void SetupBogusAppearance()
{
    [UIView appearance].jw_bogus = YES;
}

Of course, there are problems with this approach. Because it’s merely tricky and not evil, it’s not quite as efficient as it could be. If you move a tree of views from one view hierarchy to another, all the views in the moving hierarchy will be notified when they’re added to the new view hierarchy instead of just their root view. This could be inefficient. In a recent project I where I tried to use this, the bottom up notification of UI_APPEARANCE_SELECTORS caused an infinite notification loop in my code. I couldn’t be bothered to try figuring out how to solve it and reached for a bigger hammer instead.

However, with a little cleverness UI_APPEARANCE_SELECTOR can be a tool for a lot more than just setting colours and fonts on your custom views.