First, decide which areas of the app you want to skin and when you want to provide users with the option to switch skins.
I'm going to assume that you want to alter the colours of the text and the graphics and that it's acceptable for the user to have to restart the app in order to change the skin (that will make things simpler for now).
Make a plist with all of your skinnable graphics and colours. The plist will be a dictionary with appropriate, theme-neutral key names for the images and colours (for example, call red "primaryHeadingColor" instead of "red"). Colours may be represented by hex strings and images by file names.
+ (ThemeManager *)sharedManager
{
static ThemeManager *sharedManager = nil;
if (sharedManager == nil)
{
sharedManager = [[ThemeManager alloc] init];
}
return sharedManager;
}
The ThemeManager class will have an NSDictionary property called "styles", and in the init method you will load the theme into your styles dictionary like this:
- (id)init
{
if ((self = [super init]))
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *themeName = [defaults objectForKey:@"theme"] ?: @"default";
NSString *path = [[NSBundle mainBundle] pathForResource:themeName ofType:@"plist"];
self.styles = [NSDictionary dictionaryWithContentsOfFile:path];
}
return self;
}
Since applications lack a css stylesheet counterpart, this is rather challenging.
(Note: Some individuals dislike exerting a lot of effort within an init approach. Although I've never experienced a problem with it, if you like, you can construct a separate method to load the themes dictionary and call it from the setup code of your programme.
Observe how I'm using user defaults to retrieve the name for the theme plist. So, if a user chooses a theme in their preferences, saves it, and then launches the app later, it will use that theme. If no theme is chosen, I've included a default theme with the name "default," so make sure you have a default.plist theme file (or alter the @"default" in the
code to whatever the real name of your default theme plist is).
You need to use your theme now that it has been loaded; presuming your app contains a variety of images and text labels. This phase is simple if you're loading and laying those out in code. It's a little difficult if you're doing it in nibs, but I'll describe how to manage that later.
Normally, you would say: "Loading" to load an image.
UIImage *image = [UIImage imageNamed:@"myImage.png"];
But if you want that image to be themable, you'll now need to load it by saying
NSDictionary *styles = [ThemeManager sharedManager].styles;
NSString *imageName = [styles objectForKey:@"myImageKey"];
UIImage *image = [UIImage imageNamed:imageName];
This will load the themed picture that matches the key "myImageKey" in your theme file. Your style will change depending on whatever theme file you've loaded.
You might wish to include those three lines in a function because you'll use them frequently. It would be a fantastic idea to add a category to UIImage that designates a method with a name like:
+ (UIImage *)themeImageNamed:(NSString *)key;
You can then use it by simply substituting any calls to [UIImage imageNamed:@"foo.png"] with [UIImage themeImageNamed:@"foo"], where foo now refers to the theme key rather than the actual image name.
The process of theming your photographs is now complete. Pretend you're now establishing your label colours by saying: Let's say you want to theme your label colours.
someLabel.color = [UIColor redColor];
You would now replace that with:
NSDictionary *styles = [ThemeManager sharedManager].styles;
NSString *labelColor = [styles objectForKey:@"myLabelColor"];
someLabel.color = [UIColor colorWithHexString:labelColor];
You've probably noticed that UIColor lacks the method "colorWithHexString:"; you'll need to add it by creating a category. To get code to do that, search "UIColor with hex string" solutions on Google, or check out this useful category I created: https://github.com/nicklockwood/ColorUtils
If you've been paying attention, you may also be considering adding a method to UIColor with the following name rather than repeatedly writing those three lines:
+ (UIColor *)themeColorNamed:(NSString *)key;
Similar to how we handled UIImage? Great concept!
That's it, then. Any image or label in your app can now be themed. The font name and a variety of other potentially customizable aesthetic features might both be set using the same approach.
We've forgotten one little thing, though.
These strategies won't work if you've created the majority of your views as nibs (and I don't see why you wouldn't), as your image names and font colours are hidden inside impenetrable nib data and aren't being set in your source code.
There are several methods to resolve this:
1) You could duplicate your nibs and theme them, then load the nib names into your theme plist. That's not too bad, just implement the nibName method of your view controllers like this:
- (NSString *)nibName
{
NSDictionary *styles = [ThemeManager sharedManager].styles;
return [styles objectForKey:NSStringFromClass([self class])];
}
Because you can create a base ThemeViewController with that method and have all your themable view controllers inherit from it, you can save yourself some typing by noticing my clever approach of utilising the class name of the view controller as the key.
However, if you need to alter any screens later, this method requires storing several copies of each nib, which is a maintenance nightmare.
2) You could create IBOutlets for each of your imageViews and labels, then set the colours and pictures for each of them in your viewDidLoad function. The process is arguably the most time-consuming, but at least you won't need to keep multiple nibs (this is effectively the same issue as localising nibs, by the way, and is very similar).
3) You could create a unique subclass of UILabel called ThemeLabel that, when the label is instantiated, uses the code above to automatically set the font colour. You could then use these ThemeLabels in your nib files in place of standard UILabels by setting the label's class in Interface Builder to ThemeLabel. Unfortunately, you will need to develop a distinct UILabel subclass for each style if you have more than one font or font colour.
Alternatively, you could be crafty and utilise the accessibilityLabel attribute or the view tag as the style dictionary key, allowing you to use a single ThemeLabel class and the accessibility label setting in Interface Builder to choose the style.
Create a subclass of UIImageView for ImageViews called ThemeImageView that, in the awakeFromNib method replaces the image with a theme image based on the tag or accessibilityLabel property. to use the same trick.
The fact that option 3 reduces coding is why I personally prefer it. The ability to switch themes at runtime is another benefit of option 3, since you may create a system where your theme manager reloads the theme dictionary before broadcasting an NSNotification instructing all ThemeLabels and ThemeImageViews to redraw. It would most likely only require an additional 15 lines of code.
You now have a full-featured solution for theming iOS apps. Thank you very much!