Customizing Appearance With Resizable ImagesJul 5, 2012 · 7 minute read
Prior to iOS 5 it was not easy to style the appearance of common UIKit controls such as the navigation bar or the tab bar. It could be done but you had to resort to subclassing or create your own custom replacements. With iOS 5, Apple added instance methods to many user interface elements to allow the appearance to be directly customised. They also introduced the UIAppearance protocol to allow the styling or theming of all instances of a class. A detailed discussion of UIAppearance is a subject for a future post. In this post I want to explore the way you can use resizable images when setting background images for classes such as the navigation bar.
Applying a Tint
The ability to set a tint color has been present in many, but not all, user interface elements going back to iOS 2.0. With iOS 5 Apple added the tintColor property to more items such as the UITabBar and UIButton. When the tintColor exists it provides a quick and easy way to customise the colour. For example, in the case of a UINavigationBar you might set the tint color in the application delegate as follows:
UINavigationController *navController = (UINavigationController *)self.window.rootViewController; UINavigationBar *navBar = navController.navigationBar; navBar.tintColor = [UIColor orangeColor];
The system automatically applies a gradient effect to the tint color when displaying the navigation bar:
Using a Background Image
When setting the tint color is insufficient you can use a background image to apply textures or other designs to many of the iOS user interface elements. For the rest of this post I am going to limit the discussion to the navigation bar for simplicity but the discussion and technique is applicable to many interface elements in iOS 5.
On a standard, non-retina, iPhone device the navigation bar in portrait mode will have dimensions of 320 pixels wide by 44 pixels high. However most of the time you do not want to create a background image of those dimensions.
As well as being wasteful of system memory and performance you also have the issue of the image being stretched when the device is rotated or when you want to reuse the image for the iPad version of the App. Creating separate images for all variations of device, orientation and retina display would be a pain and luckily is unnecessary.
Setting A Solid Colour Using A 1x1 Pixel Image
If all you want is to set the background to a solid colour you actually only need to create a 1x1 pixel image. The system will take care of tiling the image horizontally and vertically to fill the background area of the UI element. I should add the caveat that we actually will also need a 2x2 pixel version of the image to allow for the double resolution retina display.
So with two image files as follows both containing solid orange pixels of the specified dimensions:
- orange.png - dimensions 1x1 pixels
- firstname.lastname@example.org - dimensions 2x2 pixels
The background image of the navigation bar can be set as follows:
UIImage *navBarImage = [UIImage imageNamed:@"orange"]; [navBar setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsDefault]; [navBar setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsLandscapePhone];
Since the navigation bar has different heights when rotated we need to set the background image for each orientation. The method -setBackgroundImage:forBarMetrics: takes an enumerated type (UIBarMetrics) which has two possible values as follows:
- UIBarMetricsDefault - the default bar metric.
- UIBarMetricsLandscapePhone - landscape iPhone bar metrics
Note that on an iPad the navigation bar is always 44 pixels (non retina display) regardless of the device orientation so is covered by the UIBarMetricsDefault option.
The resulting navigation bar is shown below. The difference is subtle but if you compare it to the previous image using just a tint colour you should see that this time there is no gradient effect.
Using A 1 Pixel Wide Image With A Gradient
To apply our own gradient effect to the background image we need image files that are 1 pixel wide but with a height that matches the UI element background. The image in this case will only need to be tiled horizontally to fill the background image area of the navigation bar. For the standard navigation bar we can therefore create the following images containing our own custom gradient:
- gradient-orange.png - dimensions 1x44 pixels
- email@example.com - dimensions 2x88 pixels
- gradient-orange-landscape.png - dimensions 1x32 pixels
- firstname.lastname@example.org - dimensions 2x64 pixels
Note that this time we do need to take into account the thinner navigation bar on an iPhone in landscape orientation. This has a height dimension of 32 pixels on a standard display and 64 pixels on a retina display.
The code to set the background images in this case is similar to before except that we now have different images for the two bar metrics:
UIImage *navBarImage = [UIImage imageNamed:@"gradient-orange"]; UIImage *navBarLandscapeImage = [UIImage imageNamed:@"gradient-orange-landscape"]; [navBar setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsDefault]; [navBar setBackgroundImage:navBarLandscapeImage forBarMetrics:UIBarMetricsLandscapePhone];
The appearance of the navigation bar in portrait orientation with our own custom gradient is now as shown below:
Using End Caps
So far the differences in the navigation bar appearance have been subtle but this technique really becomes useful when you want to apply end caps to the background image. An end cap is used when for example you want to have a button with rounded corners. Another (bad?) example might be to apply some of fake stitching effects to the edges of the toolbar.
The end caps as the name implies define the area of an image that is not resized when the image is stretched or tiled to fill an area. To illustrate the concept consider an image with two end caps that contain a stitching effect as shown below:
The end caps are each 8 pixels wide and are separated by an image section that is one pixel wide similar to the image used in the previous gradient example. This makes the image a total of 17 pixels wide. The height of the image is 44 pixels in this case to match our standard navigation bar. When this image is used to set the background of a navigation bar we want the central 1 pixel section to be tiled to fit but the cap ends should not be resized.
The UIImage instance method -resizableImageWithCapInsets: was introduced in iOS 5 and allows us to add cap ends to any existing UIImage. The caps are defined using an UIEdgeInsets structure which specifies the top, left, bottom and right insets to apply to our original image. In this case we can define our two end caps by insetting the existing image from the left and right edges by 8 pixels. So assuming our original 17x44 pixel image is named navbar.png we can create an image with two 8 pixel wide end caps as follows:
UIImage *navBarImage = [UIImage imageNamed:@"navbar"]; navBarImage = [navBarImage resizableImageWithCapInsets:UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)];
Note that in this case we only have two end caps so we specify 0.0 for the top and bottom insets. The image files we need to cover the retina and landscape variations are described below:
- navbar.png - dimensions 17x44 pixels
- email@example.com - dimensions 34x88 pixels
- navbar-landscape.png - dimensions 17x32 pixels
- firstname.lastname@example.org - dimensions 34x64 pixels
I will not show each image but for example here is the actual email@example.com image that I will use:
The code to create the stitched navigation bar for both sizes of the bar is as follows:
UIImage *navBarImage = [UIImage imageNamed:@"navbar"]; navBarImage = [navBarImage resizableImageWithCapInsets:UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)]; UIImage *navBarLandscapeImage = [UIImage imageNamed:@"navbar-landscape"]; navBarLandscapeImage = [navBarLandscapeImage resizableImageWithCapInsets:UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)]; [navBar setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsDefault]; [navBar setBackgroundImage:navBarLandscapeImage forBarMetrics:UIBarMetricsLandscapePhone];
The final result is shown below. I hope it is unnecessary to state the obvious but just in case I am not claiming this is a good design for a navigation bar…
As I mentioned previously this technique of resizing images can be applied when setting the images used by many of the standard UIKit user interface elements. For the navigation bar example the images are only resized horizontally but you can also resize vertically if required. Just remember that if you have a repeating pattern you will need to create the image with dimensions that match the area you want to be tiled.