Update: Updated for Xcode 8 and Swift 3 by Ron Kliffer. Original tutorial by Essan Parto.
Although skeuomorphism in iOS apps is a thing of the past, that doesn’t mean you’re limited to the stock appearance of controls in your iOS app.
True, you can develop your own controls and app stylings from scratch, but Apple recommends you use standard UIKit controls, and take advantage of the various customization techniques in iOS. This is because UIKit controls are highly efficient, and your customizations to the controls should be mostly future-proof.
In this UIAppearance tutorial, you’ll use some basic UI customization techniques to customize a plain Pet Finder app and make it stand out from the pack! :]
As an added bonus, you’ll learn how to automatically switch your app to a dark theme when opened at night.
Getting Started
Download the starter project for this tutorial here. The app has many of the standard UIKit controls and looks extremely vanilla.
Open the project and have a look around to get a feel for its structure. Build and run the app, you’ll see the main UI elements of Pet Finder:
There’s a navigation bar and a tab bar. The main screen shows a list of pets. Tap a pet to see some details about it. There’s a search screen as well and — aha! A screen that allows you to select a theme for your app. That sounds like a pretty good place to start!
UIAppearance: Supporting Themes
Many apps don’t allow users to select a theme, and it’s not always advisable to ship an app with a theme selector. However, there are cases where themes could be very useful. You might want to test different themes during development to see which ones work best for your app. You might A/B test your app with your beta users to see which style is the most popular. Or you might want to ease your user’s eyes by adding a dark theme for night times.
In this UIAppearance tutorial, you’ll create a number of themes for your app that you can try out to see which one is most aesthetically pleasing.
Select File\New\File… and choose iOS\Source\Swift File. Click Next and type Theme as the name of the file. Finally click Create.
Replace the contents of the file with the following:
import UIKit
enum Theme: Int {
//1
case `default`, dark, graphical
//2
private enum Keys {
static let selectedTheme = "SelectedTheme"
}
//3
static var current: Theme {
let storedTheme = UserDefaults.standard.integer(forKey: Keys.selectedTheme)
return Theme(rawValue: storedTheme) ?? .default
}
}
Let’s see what this code does:
- Defines three types of themes – default, dark and graphical
- Defines a constant to help you access the selected theme
- Defines a read-only computed type property for the selected theme. It uses
UserDefaults
to persist the current theme, and returns the default theme if none were previously selected.
Now you have your Theme enum set up, let’s add some style to it. Add the following code to the end of Theme
before the closing brace:
var mainColor: UIColor {
switch self {
case .default:
return UIColor(red: 87.0/255.0, green: 188.0/255.0, blue: 95.0/255.0, alpha: 1.0)
case .dark:
return UIColor(red: 255.0/255.0, green: 115.0/255.0, blue: 50.0/255.0, alpha: 1.0)
case .graphical:
return UIColor(red: 10.0/255.0, green: 10.0/255.0, blue: 10.0/255.0, alpha: 1.0)
}
}
This defines a mainColor
that’s specific to each particular theme.
Let’s see how this works. Open AppDelegate.swift and add the following line to application(_:didFinishLaunchingWithOptions:)
:
print(Theme.current.mainColor)
Build and run the app. You should the following printed to the console:
UIExtendedSRGBColorSpace 0.341176 0.737255 0.372549 1
At this point, you have three themes and can manage them through Theme
. Now it’s time to go use them in your app.
Applying Themes to Your Controls
Open Theme.swift, add the following method to the bottom of Theme
:
func apply() {
//1
UserDefaults.standard.set(rawValue, forKey: Keys.selectedTheme)
UserDefaults.standard.synchronize()
//2
UIApplication.shared.delegate?.window??.tintColor = mainColor
}
Here’s a quick run-through of the above code:
- Persist the selected theme using
UserDefaults
.
- Apply the main color to the
tintColor
property of your application’s window. You’ll learn more about tintColor
in just a moment.
Now the only thing you need to do is call this method.
Open AppDelegate.swift and replace the print()
statement you added earlier with the following:
Theme.current.apply()
Build and run the app. You’ll see your new app looks decidedly more green:
Navigate through the app. There’s green accents everywhere! But you didn’t change any of your controllers or views. What is this black — er, green — magic?! :]
Applying Tint Colors
Since iOS 7, UIView
has exposed the tintColor
property. This is often used to define the primary color for interface elements throughout an app.
When you specify a tint for a view, it’s automatically propagated to all subviews in that view’s view hierarchy. Since UIWindow
inherits from UIView
, you can specify a tint color for the entire app by setting the window’s tintColor
. That’s exactly what you did in apply()
above.
Click on the Gear icon in the top left corner of your app. A table view with a segmented control slides up. When you select a different theme and tap Apply, nothing changes. Time to fix that.
Open SettingsTableViewController.swift and add these lines to applyTheme(_:)
, just above dismissAnimated()
:
if let selectedTheme = Theme(rawValue: themeSelector.selectedSegmentIndex) {
selectedTheme.apply()
}
Here you call the method you added to Theme
, which sets the selected theme’s mainColor
on the root UIWindow
.
Next, add the following line to the bottom of viewDidLoad()
. This will select the theme persisted to UserDefaults
when the view controller is first loaded:
themeSelector.selectedSegmentIndex = Theme.current.rawValue
Build and run the app. Tap the settings button, select Dark, and then tap Apply. The tint in your app will change from green to orange right before your eyes:
Eagle-eyed readers likely noticed these colors were defined in mainColor
, in Theme
.
But wait, you selected Dark, and this doesn’t look dark. To get this effect working, you’ll have to customize a few more things.
Customizing the Navigation Bar
Open Theme.swift and add the following two properties to Theme
:
var barStyle: UIBarStyle {
switch self {
case .default, .graphical:
return .default
case .dark:
return .black
}
}
var navigationBackgroundImage: UIImage? {
return self == .graphical ? UIImage(named: "navBackground") : nil
}
These methods simply return an appropriate bar style and background image for the navigation bar for each theme.
Next, add the following lines to the bottom of apply()
:
UINavigationBar.appearance().barStyle = barStyle
UINavigationBar.appearance().setBackgroundImage(navigationBackgroundImage, for: .default)
Okay — why does this work here? Shouldn’t you be accessing a UINavigationBar
instance?
UIKit has an informal protocol called UIAppearance
that most of its controls conform to. When you call appearance()
on UIKit classes— not instances —it returns a UIAppearance
proxy. When you change the properties of this proxy, all the instances of that class automatically get the same value. This is very convenient as you don’t have to manually style each control after it’s been instantiated.
Build and run the app. Select the Dark theme and the navigation bar should now be much darker:
This looks a little better, but you still have some work to do.
Next, you’ll customize the back indicator. iOS uses a chevron by default, but you can code up something far more exciting! :]
Customizing the Navigation Bar Back Indicator
This change applies to all themes, so you only need to add the following lines to apply()
in Themes.swift:
UINavigationBar.appearance().backIndicatorImage = UIImage(named: "backArrow")
UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrowMask")
Here you’re simply setting the image and transition mask image to be used as the back indicator.
Build and run the app. Tap one of the pets and you should see the new back indicator:
Open Images.xcassets and find the backArrow image in the Navigation group. The image is all black, but in your app it takes on the tint color of your window and it just works.
But how can iOS just change the bar button item’s image color, and why doesn’t it do that everywhere?
As it turns out, images in iOS have three rendering modes:
- Original: Always use the image “as is” with its original colors.
- Template: Ignore the colors, and use the image as a stencil. In this mode, iOS uses only the shape of the image, and colors the image itself before rendering it on screen. So when a control has a tint color, iOS takes the shape from the image you provide and uses the tint color to color it.
- Automatic: Depending on the context in which you use the image, the system decides whether it should draw the image as “original” or “template”. For items such as back indicators, navigation control bar button items and tab bar images, iOS ignores the image colors by default. You can override this behavior and change the rendering mode manually.
Head back to the app, tap one of the pets and tap Adopt. Watch the animation of the back indicator in the navigation bar carefully. Can you see the problem?
When the Back text transitions to the left, it overlaps the indicator and looks pretty bad:
To fix this, you’ll have to change the transition mask image.
Update the line where you set backIndicatorTransitionMaskImage
in apply()
, in Theme.swift, to the following:
UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrow")
Build and run the app. Once again tap one of the pets and then tap Adopt. This time the transition looks much better:
The text is no longer cut off and looks like it goes underneath the indicator. So, what’s happening here?
iOS uses all the non-transparent pixels of the back indicator image to draw the indicator. However, it does something entirely different with the transition mask image. It masks the indicator with the non-transparent pixels of the transition mask image. So when the text moves to the left, the indicator is only visible in the those areas.
In the original implementation, you provided an image that covered the entire surface of the back indicator. That’s why the text remained visible through the transition. Now you’re using the indicator image itself as the mask, it looks better. But if you look carefully, you’ll see the text disappeared at the far right edge of the mask, not under the indicator proper.
Look at the indicator image and the “fixed” version of the mask in your image assets catalog. You’ll see they line up perfectly with each other:
The black shape is your back indicator and the red shape is your mask. You want the text to only be visible when it’s passing under the red area and hidden everywhere else.
To do this, change the last line of apply()
once again, this time to use the updated mask:
UINavigationBar.appearance().backIndicatorTransitionMaskImage = UIImage(named: "backArrowMaskFixed")
Build and run the app. For the last time, tap one of the pets and then tap Adopt. You’ll see the text now disappears under the image, just as you anticipated it would:
Now your navigation bar is pixel perfect, it’s time to give the tab bar some much-needed love.
Customizing the Tab Bar
Still in Theme.swift, add the following property to Theme
:
var tabBarBackgroundImage: UIImage? {
return self == .graphical ? UIImage(named: "tabBarBackground") : nil
}
This property will provide appropriate tab bar background images for each theme.
To apply these styles, add the following lines to apply()
.
UITabBar.appearance().barStyle = barStyle
UITabBar.appearance().backgroundImage = tabBarBackgroundImage
let tabIndicator = UIImage(named: "tabBarSelectionIndicator")?.withRenderingMode(.alwaysTemplate)
let tabResizableIndicator = tabIndicator?.resizableImage(
withCapInsets: UIEdgeInsets(top: 0, left: 2.0, bottom: 0, right: 2.0))
UITabBar.appearance().selectionIndicatorImage = tabResizableIndicator
Setting the barStyle
and backgroundImage
should be familiar by now. It’s done exactly the same way you did for UINavigationBar
previously.
In the final three lines of code above, you retrieve an indicator image from the asset catalog and set its rendering mode to .AlwaysTemplate
. This is an example of one context where iOS doesn’t automatically use template rendering mode.
Finally, you create a resizable image and set it as the tab bar’s selectionIndicatorImage
.
Build and run the app. You’ll see your newly themed tab bar:
The dark theme is starting to look more, well, dark! :]
See the line below the selected tab? That’s your indicator image. Although it’s only 6 points high and 49 points wide, iOS stretches this to the full width of the tab at run time.
The next section covers resizeable images and how they work.
Customizing a Segmented Control
One element that hasn’t changed yet is the segmented control that shows the currently selected theme. Time to bring that control into the wonderful world of theming.
Add the following code to the bottom of apply()
in Theme.swift:
let controlBackground = UIImage(named: "controlBackground")?
.withRenderingMode(.alwaysTemplate)
.resizableImage(withCapInsets: UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3))
let controlSelectedBackground = UIImage(named: "controlSelectedBackground")?
.withRenderingMode(.alwaysTemplate)
.resizableImage(withCapInsets: UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3))
UISegmentedControl.appearance().setBackgroundImage(controlBackground,
for: .normal,
barMetrics: .default)
UISegmentedControl.appearance().setBackgroundImage(controlSelectedBackground,
for: .selected,
barMetrics: .default)
To understand the code above, first take a look at the controlBackground image in your asset catalog. The image may be tiny, but iOS knows exactly how to use it to draw the borders of your UISegmentedControl
, as it’s been pre-sliced and is resizable.
What does sliced mean? Take a look at the following magnified model:
There are four 3×3 squares, one in each corner. These squares are left untouched when resizing the image. The gray pixels however, get stretched horizontally and vertically as required.
In your image, all the pixels are black and assume the tint color of the control. You instruct iOS how to stretch the image using UIEdgeInsets()
. You pass 3
for the top, left, bottom and right parameters since your corners are 3×3.
Build and run the app. Tap the Gear icon in the top left and you’ll see the UISegmentedControl
now reflects your new styling:
The rounded corners are gone and have been replaced by your 3×3 square corners.
Now you’ve tinted and styled your segmented control, all that’s left is to tint the remaining controls.
Close the settings screen in the app, and tap the magnifier in the top right corner. You’ll see another segmented control, along with a UIStepper
, UISlider
, and UISwitch
still need to be themed.
Grab your brush and drop cloths — you’re going painting! :]
Customizing Steppers, Sliders, and Switches
To change the colors of the stepper, add the following lines to apply()
in Theme.swift:
UIStepper.appearance().setBackgroundImage(controlBackground, for: .normal)
UIStepper.appearance().setBackgroundImage(controlBackground, for: .disabled)
UIStepper.appearance().setBackgroundImage(controlBackground, for: .highlighted)
UIStepper.appearance().setDecrementImage(UIImage(named: "fewerPaws"), for: .normal)
UIStepper.appearance().setIncrementImage(UIImage(named: "morePaws"), for: .normal)
You’ve used the same resizable image as you did for UISegmentedControl
. The only difference here is UIStepper
segments become disabled when they reach their minimum or maximum values, so you also specified an image for this case as well. To keep things simple, you re-use the same image.
This not only changes the color of the stepper, but you also get some nice image buttons instead of the boring + and – symbols.
Build and run the app. Open Search to see how the stepper has changed:
UISlider
and UISwitch
need some theme lovin’ too.
Add the following code to apply()
:
UISlider.appearance().setThumbImage(UIImage(named: "sliderThumb"), for: .normal)
UISlider.appearance().setMaximumTrackImage(UIImage(named: "maximumTrack")?
.resizableImage(withCapInsets:UIEdgeInsets(top: 0, left: 0.0, bottom: 0, right: 6.0)), for: .normal)
UISlider.appearance().setMinimumTrackImage(UIImage(named: "minimumTrack")?
.withRenderingMode(.alwaysTemplate)
.resizableImage(withCapInsets:UIEdgeInsets(top: 0, left: 6.0, bottom: 0, right: 0)), for: .normal)
UISwitch.appearance().onTintColor = mainColor.withAlphaComponent(0.3)
UISwitch.appearance().thumbTintColor = mainColor
UISlider
has three main customization points: the slider’s thumb, the minimum track and the maximum track.
The thumb uses an image from your assets catalog. The maximum track uses a resizable image in original rendering mode so it stays black regardless of the theme. The minimum track also uses a resizable image, but you use template rendering mode so it inherits the tint of the template.
You’ve modified UISwitch
by setting thumbTintColor
to the main color. You set onTintColor
as a slightly lighter version of the main color, to bump up the contrast between the two.
Build and run the app. Tap Search and your slider and switch should appear as follows:
Your app has become really stylish, but dark theme is still missing something. The table background is too bright. Let’s fix that.
Customizing UITableViewCell
In Theme.swift, add the following properties to Theme
:
var backgroundColor: UIColor {
switch self {
case .default, .graphical:
return UIColor.white
case .dark:
return UIColor(white: 0.4, alpha: 1.0)
}
}
var textColor: UIColor {
switch self {
case .default, .graphical:
return UIColor.black
case .dark:
return UIColor.white
}
}
These define the background color you’ll use for your table cells, and the text color for the labels in it.
Next, add the following code to the end of apply()
:
UITableViewCell.appearance().backgroundColor = backgroundColor
UILabel.appearance(whenContainedInInstancesOf: [UITableViewCell.self]).textColor = textColor
The first line should look familiar, it simply sets the backgroundColor
of all UITableViewCell
instances. The second line however is where things get a little more interesting.
UIAppearance
let’s you condition the changes you want to make. In this case, you don’t want to change the entire app’s text color. You only want to change the text color inside UITableViewCell
. By using whenContainedInInstancesOf:
you do exactly that. You force this change to apply only to UILabel
instances inside a UITableViewCell
.
Build and run the app, choose dark theme, and the screen should look like this:
Now this is a real dark theme!
As you’ve seen by now, the appearance proxy customizes multiple instances of a class. But sometimes you don’t want a global appearance for a control. In these cases, you can customize just a single instance of a control.
Customizing a Single Instance
Open SearchTableViewController.swift and add the following lines to viewDidLoad()
:
speciesSelector.setImage(UIImage(named: "dog"), forSegmentAt: 0)
speciesSelector.setImage(UIImage(named: "cat"), forSegmentAt: 1)
Here you’re simply setting the image for each segment in the species selector.
Build and run the app. Open Search and you’ll see the segmented species selector looks like this:
iOS inverted the colors on the selected segment’s image without any work on your part. This is because the images are automatically rendered in Template mode.
What about selectively changing the typeface on your controls? That’s easy as well.
Open PetViewController.swift and add the following line to the bottom of viewDidLoad()
:
view.backgroundColor = Theme.current.backgroundColor
Build and run the app. Select a pet, and look at the result:
You’re done with the styling. The image below shows the before and after results of the Search screen:
I think you’ll agree the new version is much less vanilla and much more interesting than the original. You’ve added 3 dazling styles and spiced up the app.
But why don’t you take it a step further? What if you helped the user by opening the app with the appropriate theme according to when he opened it? What if you switched to dark theme as the sun sets? Let’s see how we can make that happen.
Automating dark theme with Solar
For this part of the tutorial, you’ll use an open source library called Solar. Solar receives a location and date, and returns the sunrise and sunset times for that day. Solar comes installed with the starter project for this tutorial.
Open AppDelegate.swift and add the following property below var window
:
private let solar = Solar(latitude: 40.758899, longitude: -73.9873197)!
This defines a property of type Solar
with today’s date and a given location. You can of course change the location so it’ll be taken dynamically according to the user’s location.
Still in AppDelegate
, add the following two methods:
//1
func initializeTheme() {
//2
if solar.isDaytime {
Theme.current.apply()
scheduleThemeTimer()
} else {
Theme.dark.apply()
}
}
func scheduleThemeTimer() {
//3
let timer = Timer(fire: solar.sunset!, interval: 0, repeats: false) { [weak self] _ in
Theme.dark.apply()
//4
self?.window?.subviews.forEach({ (view: UIView) in
view.removeFromSuperview()
self?.window?.addSubview(view)
})
}
//5
RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
}
Let’s see what’s going on here:
- You define a method to initialize theme settings
Solar
has a convenience method to check if the time it was given is in daytime. If so, you apply the current theme and schedule a Timer
that will change themes.
- You create a timer and set it to fire at sunset.
- This is an important thing to notice. Whenever
UIAppearance
changes values, they will not be reflected until the view re-renders itself. By removing and re-adding all the views to their window, you assure they are re-rendered according to the new theme.
- You schedule the timer by adding it to the main
RunLoop
Finally, in application(_:didFinishLaunchingWithOptions:)
, replace:
Theme.current.apply()
with:
initializeTheme()
Open the app after sunset (You can change the time on your phone if you’re reading this during the day). The app should appear with dark theme selected.
Where to Go From Here?
You can download the finished project with all the tweaks from this tutorial here.
In addition to the tweaks you’ve already made, UIAppearance
also supports customizing controls for a specific UITraitConnection
. This let’s you support multiple themes for different device layouts.
I hope you enjoyed this UIAppearance tutorial and learned how easy it can be to tweak your UI. If you have any comments or questions about this tutorial, please join the forum discussion below!
The post UIAppearance Tutorial: Getting Started appeared first on Ray Wenderlich.