The best way to customize your UI
iOS developers are all too familiar with UIControl and its many pre-built subclasses. By now, you've probably memorized all of them, the best way to interact and display them, and the various properties or instance methods you can use to customize their appearance. You might have even subclassed UIControl and built something completely your own. And as many of you know, doing this customization instance by instance can be a royal pain.
The easiest way to customize your controls is by using UIView's .tintColor property, a concept introduced in iOS 7. If you aren't already familiar with the details of how tint color works, you should really check out Apple's UIView documentation on tint color, as well as their WWDC '13 video on the concept — they're essential for your to know. The short explanation is this: The tint color acts like that control's theme color, and indicates the interactive portions of the control. The tintColor property isn no ordinary instance property -- it inherits its default value from its parent view, meaning you can customize the tint color of every view in your app by changing the app window's tint color. Take a look:
I dragged a number of controls and placed them on a single view controller via Interface Builder. In the first example, I wrote no code whatsoever and in the second example, I added the following line to my -application:didFinishLaunchingWithOptions: method of my app's delegate object:
self.window.tintColor = [UIColor purpleColor];
Again, if you haven't read the tint color documentation you should probably do that first, but here are a few important takeaways:
1. Changing the tint color affected the way a control looks, but it doesn't affect the entire control. The knob of the slider and the empty space of the progress view remain unchanged. Tint color is not a replacement for other control-specific instance properties used for customization.
2. Not all controls are automatically affected by tint color -- it's up to the implementation of the subclass to take advantage of the .tintColor property. UISwitch, for instance is not affected by the .tintColor property, and has class specific properties like .onTintColor and .thumbTintColor. For you custom controls classes, you'll need to decide how, if at all, you will take advantage of the .tintColor property.
3. Controls who's tint color is explicitly assigned will not automatically inherent tint color. The UIActivityIndicator, for instance, was explicitly instantiated with UIActivityIndicatorViewStyleGrey, and thus does not automatically inherit tint color. The .tintColor property, however, *does* affect the color of the activity indicator. Similarly, you will need to implement this behavior yourself if you want initializers to override the provided tint color and not absorb the superview's tint color when added to the view hierarchy, by assigning a value to .tintColor before the view is added to the hierarchy.
The Appearance Proxy
What about customizing controls outside of tint color? There are plenty of other ways to further customize controls outside of basic tint color limitations, and there are static views like UILable which do nothing at all with tint color. Because of this, many developers who use tintColor exclusively often find themselves writing code in every view controller to style every component as needed. But what if you could change Apple's default values?
In this example, I've changed the app window's tint color to purple, but I've also used the appearance proxy API make some other changes to UISwitch, UISlider, and UIProgressView. Take a look at the results:
And look at that! Despite not having written a single piece of style specific code in my view controller, I've managed to have every control look exactly as I want it to!
Under the Hood
Because tint color and appearance *can* be used in similar ways, people often use them together with confusing results. However, tint color and UIAppearance work *very* differently under the hood, and understanding the difference between these two APIs is crucial when deciding on the best way to style your app.
Tint Color is a property just like any other on your UIView. It's effect on your control's looks are completely up to you, and every control implements tint color differently. By default, tint color inherits its value from its superview when added to the view hierarchy if not explicitly assigned beforehand, but again, this behavior can vary from control to control, and can be overridden for your own custom views. Apple is pretty consistent about their usage patterns apart from a few notable exceptions, and it's best that you probably conform to those.
UIAppearance is a totally different animal. It works by forwarding invocations of a instance's customizable property accessors to a dedicated class-specific proxy object, which houses all the "defaults" for a given class. UIAppearance is smart — it applies customization just before the view is added to the view hierarchy, it keeps track of a instances' accesor method usage. UIAppearance can tell whether the setter for a given property has been called, and won't customize those instances — it assumes your instances specific customization is what you want instead.
Unlike tint color, which only affects subviews whose tint color is not explicitly defined somewhere else, UIAppearance affects every instance of the control after the changes to the appearance proxy have been made, at just the correct moment.
There's a lot of info to absorb, so lets review the highlights, shall we?
1. Tint color customization happens only after the view has been added to the view hierarchy, because its customization depends on its super view. UIAppearance, on the other hand, happens before the view has been added.
2. Tint color customization will happen to any instance who's tint color is not explicitly set to something else. (though this behavior can be inconsistent if a classes initializer assigns the tint color property). As such, UIAppearance customization usually takes preference over tint color customization.
3. UIAppearance customization will happen to any property who (a) was designed to work with UIAppearance and (b) has not had it's setter method invoked during the customization point.
4. Tint color simply assigns a value to instances who's .tintColor values which have not been provided at the customization point, but tint color is just a property like any other. This means that you can even use UIAppearance to control tint color's default values.
5. The effects from tint color on a class's style are not consistent, and maybe non-existent.
6. Per instance customization always wins. Unless you've built a custom control to explicitly not do this, Any customization that happens directly to the instance of the view post initialization will win.
So how do we use the appearance proxy API? Well, it's actually pretty simple. Any object that conforms to the UIAppearance protocol is customizable via UIAppearance. Because UIView conforms to this protocol, nearly every control Apple provides you with already has this functionality. The UIAppearance protocol describes four methods you can use to customize your objects via an appearance proxy:
+ (instancetype)appearance; + (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait; + (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class<UIAppearanceContainer>> *)containerTypes; + (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedInInstancesOfClasses:(NSArray<Class<UIAppearanceContainer>> *)containerTypes;
These methods all return an object who's interface is identical to the class your trying to customize (Though under the hood, they aren't actually an instance of the class). From there, you can customize that object, and it's property values will serve as the defaults during appearance proxy customization. These four methods are fairly self explanatory, but when used in conjunction can be incredibly powerful. For example, you could change the .onTintColor of all UISwitch, except when they are contained in a table view cell:
[UISwitch appearanceWhenContainedIn:@[[UITableViewCell class]]].onTintColor = [UIColor redColor]; [UISwitch appearance].onTintColor = [UIColor orangeColor];
Check out Apple's UIAppearance documentation for more detailed info on which appearance proxies supersede others in the event of a collision.
Note that UIAppearance affects *every* instance of that class added to the hierarchy after that change, including instances that are part of the implementation of other controls. As such, you should avoid customizing say, UILabel or UIButton with the appearance proxy API, because those controls are used in other more complex controls like navigation bars or segmented controls, and your changes will affect all of them.
UIAppearance in Custom Objects
What about custom controls though? If your custom control is used in multiple places within your app with different styles, or if you plan on shipping your control for use by other developers on some way, you should consider adding support for UIAppearance. Developers will appreciate the ability to customize your control the way they already customize tons of Apple controls, and implementing things like UIAppearance is a sign of a high quality codebase. People will appreciate you going that extra mile, and it signals that you're control is designed for use in production.
If you're custom control is a subclass of UIView, then implementing UIAppearance is pretty easy, and can be accomplished in two steps:
1. In your header file, make sure your class declares its conformance to UIAppearance.
2. For every customizable property, add UI_APPEARANCE_SELECTOR to its definition.
That's pretty much it! in this sample control, take a look at the UIAppearance-customizable control "specialColor":
That's pretty much all you need for some basic support. If you want to go above and beyond and ensure that your control works exactly like Apple's, then consider the following:
1. If your view is the kind of view that might have subviews added to it, make sure it conforms to UIAppearanceContainer as well. This will all the +appearanceWhenContainedIn: style methods to work as expected.
2. Consider the use of property setters within your class implementation. UIAppearance will only customize the properties who's setter methods have never been called before. As such, DO NOT explicitly call your setter method in your object's initializer. If you do, UIAppearance will never take over.
3. Consider whether or not a strong reference to this property's value is maintained by anyone, and if so by whom. UIAppearance will be calling your setter method at some point during your view's lifetime. Make sure your view is in a position to accept the value assigned when that setter is called by UIAppearance.
If your object isn't a subclass of UIView, things get a little trickier. They you aren't going to be able to take advantage of the magic of UIAppearance anyway, because you can't add something that isn't a subclass of UIView to the view hierarchy. However, there are a couple of situations where you might want to have UIAppearance-like customization for a class that isn't a UIView subclass:
1. Objects that represent a view, but aren't actually ever added to the hierarchy. A good example of this is Apple's own UIBarButtonItem, which certainly behaves like a view but is not a subclass of UIView. The UIBar which owns the bar button time builds and manages the representative view as needed.
2. View Controllers who designed for single modal usage, like Apple's own UIAlertController. These are certainly views, but they mange their own presentation, and thus are an instance UIViewController. The actual UIView instances are not accessible.
Apple has built in UIAppearance support for UIBarButtonItem, but it's not available for you or I. As such, there are a couple of replacements that allow for native UIAppearance like customization in classes that aren't subclasses of UIView.
Custom Appearance Proxy
This is way works with by approximating Apple's implementation of the UIAppearance API. It takes advantage of NSInvocation, which is an object used to represent an Objective-C message, with properties for the target, selector, arguments, and return values. Here's a sample implementation of how one might do that, loosely based on this stack overflow post.
To do this, we start by adding the +appearance class method to our class, because it doesn't conform to the UIAppearance protocol and thus does not have definitions built into it.
Then, we built a class to serve as the appearance proxy. Only one single instance per class is needed at runtime, so my class-specific implementation below uses a shared instance with the singleton pattern via dispatch_once.
Generally speaking, you'll need two methods: one to instantiate and re-use a single instance of the proxy object for every class, as well as a way for you to forward messages to that proxy class.
Finally, you'll need to implement +appearance in your custom object, and you'll need to begin forwarding invocation on instantiation:
Now, you can customize this class like so:
[SpecialObject appearance].specialColor = [UIColor redColor];
This a fairly simple implementation, but there are better ways to do this. You could write a single appearance proxy class for use with multiple classes, and it could keep track of its own instances, and instantiate them once per class definition as needed. You could even do one per containment rule, if you decided to go that route. There is even a third party library, MZAppearance, which wraps some of this functionality up neatly, and it even declares its own protocol MZAppearance, for you to add to your classes, so you can get a compiler warning if you haven't implemented +appearance in a customizable class
Pros: Easily add appearance functionality to every property of a class, gets you pretty close to Apple like behavior. Both universal and class-specific implementations are possible.
Cons: Can be quite a bit of work, and doesn't actually create classes which conform to UIAppearance.
Fake Appearance Proxies with a Dedicated Class
This approach works by creating a dedicated UIView class to serve as your appearance proxy. Implement UIAppearance on both objects, and forward all the messages from your non-UIView object to your dedicated class-specific appearance proxy. This approach is fairly crude, as you're basically just using the other class as a storage mechanism for your defaults, and it requires a class specific and property specific implementation. It's useful for View Controllers, because they rarely need features like containment, and it also makes your class actually confirm to UIAppearance.
1. ) Define your custom view controller, and a view to serve as the appearance proxy with the same UI_APPEARANCE_SELECTOR properties.
Then, make sure to assign your default values in your view controller's designated initializers, based on the values of the appearance proxy (which will return nil if a proxy value has not yet been assigned)
This method is really crude, but it's quite a bit easier than creating your own appearance proxy, and allows for object-specific implementations of when defaults are actually assigned. It also allows for per-property support, and can be ideal for view controllers you want to be customizable.
Pros: Create classes that actually confirm to UIAppearance, a lot less boilerplate/overhead work
Cons: Requires class, specific property specific implementations, doesn't really take advantage of the magic of UIAppearance. Kind of a crude alternative.
UIAppearance is a great way to customize the look at feel of your application. It allows you to ensure that every instance of every view is customized at the exactly right moment, without requiring you to write tons to duplicate code, while also retaining access to per-instance level customization. It also allows you to easily *change* customization later down the road, without you needing to go through every source file or interface builder file one by one.
If you're working on shipping a custom control, consider adding support for UIAppearance customization if you can.
Take a look at my replacement for Apple's UIAlertController, VSAlert. It's two primary classes, VSAlertController and VSAlertAction both support customization via UIAppearance, and checkout the sample code to see examples of implementing appearance proxies in custom objects.