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.