Theming

Many of the projects on which I’ve worked have had the problem of “theming”, because it’s difficult to keep a project’s codebase clean when dealing with multiple themes. When I refer to a “theme” I mean something that only affects the look and feel of an app, but not the functionality.

By “theming”, I’m referring to a project that might have different targets, each with different colour schemes, fonts, sizes, etc. to deliver a different user experience with the same functionality.

But theming can be extended to a single app, with a single theme, where different interface builder (nib/xib) files are backed by a single class. This might be as simple as having a different interface file (or nib) for iPad as opposed to iPhone, where each nib is backed by the same class.

Theming is often even more complex. For example, I might have an app that can display the same content in various forms. A news app, for example, might have a timeline in which it displays article teasers as either: a small widget with headline only; a double-height, more-impactful widget with headline and thumbnail image; a double-height widget with headline and article summary. That’s three nibs, and I’ll also need each one styled slightly differently whether it’s on iPad or iPhone. So that’s six nibs all backed by a single class!

In this article, I’ll discuss my latest solution to “theming” using User Defined Runtime Attributes.

Theming with User Defined Runtime Attributes

Using user defined runtime attributes, you can specify the aesthetic settings of a user interface exactly at the point at which it matters, in Interface Builder. I’m a firm believer in the separation of concerns, and therefore I think the best place to configure an interface is in Interface Builder.

The objective is to be able to do something like so to specify how the app should display my view:

images

In the example above, I’ve set the value of the “themeTextAttributes” key to articleWidgetTitle.iPhone.large. I’ll use this key-value pair later, but what’s important at this stage is that this can be configured differently for each nib.

The ultimate objective is a project that requires no changes in code to change the aesthetics of a view.

Reading a Theme from a Property List

One of the traditional theming methods I’ve come across is the use of a property list (.plist file) or other resource containing keys and values for fonts, colours, etc. for a particular theme.

I first came across this approach from the Twinlogix blog post written by Alex Reggiani, in which he uses a plist file to reference each of his interface elements. His plist appears to contain one huge list of keys for various UI elements – of which there may be hundreds in a larger app – and I think this makes it very difficult to find the element you’re looking for when you want to modify something. In the second part of Alex’s article, he concludes that the best approach is to specify in the nib’s user defined runtime attributes the font name and size for a UI element. I personally don’t think this approach provides any benefit over using Interface Builder’s attribute inspector.

I have also seen a very good implementation of the plist approach by Shaps , but again this results in a very long list of keys in a dictionary, which I personally find hard to maintain. And the technique I will describe below means that all this configuration can come from inside Interface Builder, rather than in view or view controller code.

Now consider the fictional apps Sam’s Mega Lucky Bingo and Jurassic Bingo Legends, which are both built from the same codebase with exactly the same user interface. Their only difference is the colour scheme and fonts used throughout each app. For these apps, I would create a different plist for each app and compile the corresponding plist into the bundle at build time, resulting in a different look and feel. An example plist file might look like so:

images

I think this is a realy nicely structured plist, in which I can group components at various levels, so they can be easily located, along with the device type on which it will appear, and the form it will take.

Now I would likely have a helper class from which I can request the font, text colour and line spacing for a particular user interface element. For example, in the -awakeFromNib method of my ArticleWidgetView class, I would setup the view as follows:

ArticleWidgetView.m
1
2
3
4
5
6
7
- (void)awakeFromNib
{
  NSString *element = @"articleWidgetTitle.iPhone.large";
  self.titleLabel.font = [Theme fontForElement:element];
  self.titleLabel.textColour = [Theme colorForElement:element];
  // line-spacing will be used when setting the attributedText property.
}

The Theme class helper methods will look up the values for the relevant properties in the plist dictionary. The implementation of the +fontForElement: method might look like so (after having read the plist into a dictionary on app launch):

Theme.m
1
2
3
4
5
6
7
8
9
10
11
12
+ (UIFont *)fontForElement:(NSString *)element
{
  NSDictionary *textAttributes = [[Theme sharedInstance] plistDictionary][@"Text Attributes"];
  NSDictionary *elementDictionary = [textAttributes valueForKeyPath:element];

  NSString *fontName = elementDictionary[@"fontName"];
  CGFloat fontSize = [elementDictionary[@"fontSize"] floatValue];

  NSAssert(fontName, @"Font not found for element %@", element);

  return fontName ? [UIFont fontWithName:fontName size:fontSize] : nil;
}

Because the element string passed into the method takes the form of a key path (keys separated by dots), I can pass this directly to the dictionary to get the object at the end of the path. So in my -awakeFromNib method, I pass in the string @"articleWidgetTitle.iPhone.large", which I use as the string in -valueForKeyPath: method above. This is effectively the same as doing textAttributes[@"articleWidgetTitle"][@"iPhone"][@"large"].

The downside to what we’ve seen so far is that in your class implementation you need to know the “element name” that you will pass to the Theme class helper methods.

Separating Concerns

Now the objective is to strip out any interface-configuration logic from the view class. I don’t really want the ArticleWidgetView class to care about the theme at all. It is loaded from a nib, so why should there be any further configuration required in the implementation?

That’s when we get back to user defined runtime attributes. As shown before, I’ve configured my view in Interface Builder with the following runtime attributes:

images

Now as soon as I run this project, it’s going to crash:

1
2
3
*** Terminating app due to uncaught exception 'NSUnknownKeyException',
reason: '[<UILabel 0x993b840> setValue:forUndefinedKey:]:
this class is not key value coding-compliant for the key themeTextAttributes.'

We can easily fix this by implementing the necessary method in a category on UILabel, as follows:

UILabel+Theme.m
1
2
3
4
5
6
7
8
9
10
11
12
- (void)setThemeTextAttributes:(NSString *)element
{
  self.font = [Theme fontForElement:element];
  self.textColor = [Theme colorForElement:element];
  CGFloat lineSpacing = [Theme lineSpacingForElement:element];
  objc_setAssociatedObject(self, @selector(themeLineSpacing), @(lineSpacing), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (CGFloat)themeLineSpacing
{
  return [objc_getAssociatedObject(self, _cmd) floatValue];
}

By simply having this category exist in the project, all instances of UILabel will be able to respond the setThemeTextAttributes: method, which means I can use the key themeTextAttributes in Interface Builder’s user defined runtime attributes.

In the code snippet above, I’m also setting an associated object on the UILabel instance, which will hold the line spacing as specified in the theming plist. I can use this property when setting attributed text on the label in the view’s implementation. I will define the category header as follows:

UILabel+Theme.h
1
2
3
@interface UILabel (Theme)
@property (nonatomic, readonly) CGFloat themeLineSpacing;
@end

Note that I do not need to publicly expose the setThemeTextAttributes: method in the interface.

Now I can configure all my labels in Interface Builder with no need to clutter the view or view controller code with unnecessary aesthetics code. I can add further categories for UITextView, UIView and other UIKit classes so that I can set the keys for their aesthetic properties in Interface Builder - where interface configuration belongs!

Demo Project

A demo project is available on GitHub.

I’m really keen to hear how other people have tackled theming, so talk to me on twitter! Or subscribe to my RSS feed for more of the same!