Follow me to the Dark Side

Implementing a Night Mode theme in your iOS UI

It is rather uncommon to see apps built with dynamic color schemes, despite the fact that a Dark Theme never goes unnoticed. It adds a tremendous amount of value to the user experience, and it is something people like to talk about, a lot.

My team and I recently released the beta of an internal project, Coindex. It’s an app to help you track cryptocurrency prices and your holdings in the markets. We’ve had amazing feedback from our beta users (🙏 thank you all), and a surprising amount of that feedback is all about the dark theme.

Night Mode (left) and Day Mode (right) examples in Coindex

If you’re reading this, you’re probably already convinced of the value a Night Mode can add. Over the next few minutes, I’ll break down a simple strategy to implement it with a few core components, and how they work together to create a dynamic UI. Let’s get to it!


The Components

As you’d expect, there’s more than one way to skin this app, but generally your approach should consist of the following components:

  1. A paradigm for applying the theme’s attributes to the UI
  2. Attribute Categories on UIColor, UIFont, etc, to globally define attributes and provide convenience
  3. A Theme Manager, to determine/persist the current theme style

Let’s take a look at each to see how these components work to make building a robust Night Mode easy as cake 🍰.

(A sample project is linked at the bottom 😉)


Component #1: Applying your Theme

First, I want to address how to apply your theme, by explaining how not to apply your theme, via what is probably the most popular way to apply your theme, using UIAppearance.

Not all that is UI can be controlled by UIAppearance in the first place.

UIAppearance 😖

If you Google search how to implement a night mode, it’s likely UIAppearance has been recommended to you. In fact, many have made tutorials on how to use UIAppearance to support themes, and I don’t blame them. Why use it? Because it’s easy. UIAppearance is like the Storyboard of UI code, easy for simple things, but it has poor customization and scalability.

Appearance selectors start off simply. Let’s assume we want all our UILabels to have a particular text color. You might then call this upon applicationDidFinishLaunching::

[[UILabel appearance] setTextColor:textColor];

Great! 🙃 Every label that appears will now have that text color . But of course, we don’t want all of our labels to have that color. Some need color A, others color B, etc, etc. There are strategies to achieving this:

[[TitleLabelSubclass appearance] setTextColor:titleColor];
[[SubtitleLabelSubclass appearance] setTextColor:subtitleColor];

By subclassing UILabel, you can specify different appearance attributes to different labels. Why do I hate this? Because not only is it obnoxious to ensure every label is the proper subclass for its appearance, it prevents me from using custom functional subclasses as needed, without again accounting for appearance.

UIAppearance is like the Storyboard of UI code

And although there are more appearance selectors which provide more control (appearanceForTraitCollection:, appearanceWhenContainedInInstancesOfClasses:), you’ll end up overfitting everything to work, and still being unable to accomplish certain tasks. After all, not all that is UI can be controlled by UIAppearance in the first place.

Finally, if the theme changes during runtime, the method to reapply appearance changes is one which makes me cringe, by removing and re-adding all subviews of your application’s windows.

So what’s an iOS developer to do?


Hello, UIResponder! 🙌

Allow me to introduce an alternate strategy which I’ll call the Responder Approach, as it utilizes an extension on UIResponder. The Responder Approach is really quite simple, and is designed to mimic the way you already configure your UI (hopefully) — in code, per class.

Let’s take a look at the UIResponder+Theme.h category file:

@protocol UIResponderTheme <NSObject>
@optional
- (void)themeDidChange:(CFATheme)theme;
@end

@interface UIResponder (Theme) <UIResponderTheme>
- (void)registerForThemeChanges;
@end

This category extends to all subclasses of UIResponder the ability to registerForThemeChanges, which will then callback themeDidChange: upon registration and when the theme changes.

Example in a view controller

- (void)viewDidLoad {
[super viewDidLoad];
[self registerForThemeChanges];
}
- (void)themeDidChange:(CFATheme)theme {
self.title = (theme == CFAThemeDark) ? @"Dark" : @"Light";
self.view.backgroundColor = ...;
}

Example in a table cell

- (void)awakeFromNib {
[super awakeFromNib];
[self registerForThemeChanges];
}
- (void)themeDidChange:(CFATheme)theme {
self.textLabel.textColor = ...;
self.backgroundView.backgroundColor = ...;
self.selectedBackgroundView.backgroundColor = ...;
}

Easy, right? Under the hood, UIResponder+Theme is just providing standardized convenience methods to forward an NSNotification event. There’s really nothing special about it.

At this point you may be thinking, “There’s nothing special about that!” and yeah, right, I just said that 😌️. But there’s a few very important things about what we’ve done as opposed to UIAppearance:

  • Our UI configuration code can exist where it ought to (in the respective classes)
  • We’ve untethered ourselves from the constraints of maintaining subclasses & superviews to determine a view’s appearance
  • We’ve provided a mechanism which allows for executing more than what UIAppearance supports (i.e. changing the text or frame of a label, etc.)
  • We’ve standardized the approach, which ought to make our development clean 👍

Nonetheless, it may seem that the Responder approach will take more work than the Appearance approach. In order to mitigate that, we need to optimize how much work we can offload elsewhere — namely to the Attribute Categories.


Component #2: Attribute Categories

Whether or not you’re to use the Appearance or the Responder approach (or even if you won’t have a Night Mode at all), you should always have some Attribute Categories in your project. The attributes I’m talking about in particular are UIColor and UIFont. Extending these classes allows for you to globally define the sets of attributes used in your app and easily manipulate attributes in the future.

Our views ought to worry as little as possible about which theme is the current theme, and leave that overhead to our categories

Let’s examine a simple use-case, assuming our app has only one theme. Here’s what your UIColor category might have:

typedef NS_ENUM (NSInteger, CFAColor) {
CFAColorDarkGray,
CFAColorLightGray,
CFAColorBlue,
...
};
...
+ (UIColor *)colorForType:(CFAColor)type
{
switch (type) {
case CFAColorDarkGray:
return [UIColor colorWithRed:...];
case CFAColorLightGray:
return [UIColor colorWithRed:...];
case ...
}
}

It is a simple yet powerful category that keeps your code consistent, flexible, and clean.

To add our Night Mode, it need do just a little more. Our views ought to worry as little as possible about which theme is the current theme, and leave that overhead to our categories. We can do so by adding the following method:

+ (UIColor *)day:(CFAColor)dayType night:(CFAColor)nightType
{
CFATheme currentTheme = [CFAThemeManager currentTheme];
     if (currentTheme == CFAThemeDark) {
return [UIColor colorForType:nightType];
}
return [UIColor colorForType:dayType];
}

Voila! Our Color Attribute Category now handles all the logic for themes — our views now only need care about which attributes our UI elements should have.

Back in our table cell example of themeDidChange:, we can outfit it like so:

- (void)themeDidChange:(CFATheme)theme
{
self.textLabel.textColor = [UIColor day:CFAColorDarkGray night:CFAColorLightGray];
...
}

Thus, when the theme changes, the cell will reapply those attributes, without worrying which theme is current. It need only worry which attributes ought apply for which themes.

An alternate approach

While I appreciate the above as it gives my views more flexibility to pick the colors for each theme, you may find it easier (especially if you have three or more themes) to define your colors by where they’ll be used:

// ALTERNATE APPROACH - colors defined how they'll be used
typedef NS_ENUM (NSInteger, CFAColor) {
CFAColorTitleText,
CFAColorSubtitleText,
CFAColorViewBackground,
...
};
+ (UIColor *)colorForType:(CFAColor)type {
CFATheme theme = [CFAThemeManager currentTheme];
     switch (type) {
case CFAColorTitleText:
return (theme == CFAThemeDark) ? ... : ...;
case ...
}
}

Either way, the goal is accomplished: from our view’s perspective, configuring UI for Day/Night Mode is now just as simple as having no themes at all. Now, we’re ready to move onto the final component.


Component #3: The Theme Manager

Last but not least, the brains behind the operation. Our Theme Manager is a pretty straightforward singleton that accomplishes a few easy tasks:

  • Automatically calculate the current theme
  • Provide a means to enforce a theme
  • Send notification when the theme changes
// CFAThemeManager.h
extern NSString *const kThemeChangedKey; // NSNotification key
typedef NS_ENUM (NSInteger, CFATheme) {
CFAThemeLight,
CFAThemeDark
};
@interface CFAThemeManager : NSObject
+ (instancetype)sharedManager;
@property (nonatomic, strong) NSNumber *forcedTheme;
@property (nonatomic, readonly) CFATheme currentTheme;
@end

Here’s what’s happening underneath:

  • The value in forcedTheme provides a means to control the theme mode: Auto (nil), Forced Day (0), Forced Night (1).
  • When set to Auto, the app uses the device’s UIScreen brightness to calculate the current theme. If it drops below 30%, the theme is set to Night, and if it rises above 40%, it will switch back to Day.
  • On init, appBecameActive, and brightnessChanged, the Theme Manager will recalculate the theme.
  • If the theme changed, it posts the kThemeChangedKey notification within an animation block.

And that’s it! 🎉

Sample Project

I hope I’ve helped dispel any hesitations you might have about trying out a night mode. With the proper planning and structure, it’s really pretty simple.

As promised, here’s the sample project. The GIF shows it in action 👍


Want to Read More? 🤔

More on Cryptocurrencies

If you’re interested in learning more about our Cryptocurrency App, Coindex, and becoming a beta tester, check this out!

Another wonderful resource for those interested in cryptocurrencies is the Crypto Aquarium. It’s a great community for all things crypto -

More on iOS Development

For more on iOS Development, you can check out some of my previous works on Background Tasks and Window Navigation:

Thanks ✌️