How We Broke Down the Process of Adding Dark Appearance to Our iOS Enterprise Application

Itay Amzaleg
Fiverr Tech
Published in
9 min readJul 6, 2022

Over the years, Apple has perfected a set of tools that help developers support its different visual design systems, such as dark mode, accessibility, and color schemes, among others, to the point where recent technologies, like SwiftUI, make it almost ridiculous to not take all of those visual elements into consideration as part of the code-writing process.

However, when it comes to production applications with large code bases, a lot of legacy code, and backward compatibility to older iOS versions, things might become more complex.

Assuming that we already understand the “why” and “how” of dark mode as an addition to any application, in this article we’ll discuss the process of adding dark mode support to existing large projects and how we broke it down into smaller tasks that can be executed continuously alongside other sprint tasks.

Step 1 — Set Up Your Design System

Before we jump into execution, it’s crucial that we lay out the foundations for a unified design system that can scale up and update over time. It will also help us improve our communication with the designers by creating common language and naming conventions.

If you are interested in learning more about this matter, I encourage you to dedicate five minutes to reading this awesome article published by my colleague Amit Maman about the creation process on the design side of things.

Color Scheme and Asset Catalog

Assuming that your existing project already has some kind of color palette with which you are used to working, the challenge here is to group everything to one place and create representations for the different appearances.
Our first step was to create a new asset catalog only for colors. Inside, we split the palette into five different groups:

  1. Backgrounds
  2. Base Colors
  3. Separators
  4. Texts & Labels
  5. Tints

Colors with different appearances received their dark and light appearance attributes, while colors such as base black or base white, which should look the same in any mode, were granted a universal appearance.

You can read more about colors and color schemes in Apple’s Human Interface Guidelines.

The colors asset catalog

After we sorted out our asset catalog, the next step was to create a new UIColor extension to access all of those colors more easily from code.

Fonts Palette and Text Style

Similar to the color palette, we’ll assume that your existing project already has some kind of fonts palette or closed set of fonts that you use in your app.
Either you use system fonts or custom fonts (like we do on Fiverr). The same process as colors applies here, we want to group everything to one extension for easier access during later stages.

We also created different font styles for common combinations of font appearances in the app.

Assets and UI Elements

By now, using an asset catalog for images should be best practice for any application. Not only is it a great way to manage and group all of your assets, but also it’s the easiest way to set custom attributes for each image.

Given that we already manage our assets in an asset catalog, all we had to do here was to review them and set dark appearances where needed.
Assets with universal appearance were customized by the designers to fit any appearance.

Files outside of the asset catalog (e.g., gifs, videos, and Lottie animations) received the same treatment, each one either received a dark mode appearance or was adjusted for universal appearance.
The only difference here is that without the asset catalog attributes, you’ll have to reassign each asset manually in the code when the appearance changes.

Tip!

To keep things simple, we created our own wrappers to handle the appearance switch.
For example, if the Lottie resource had a dark representation, we added it as another json file with the same name and the word “Dark” as the suffix.
Then, we created an AnimationView wrapper that handles appearance switching.
Other common UIView design elements, such as shadows and borders, were moved into a UIView extension for easy access.

Applying shadow to view example:

stackView.applyShadow(type: .lowIntensity)

Custom Controls

Finally, we created a set of custom controls for commonly used controls, such as label, button, textfield, and textview. So, we can easily set our controls styles.

For example, label style contains font style (i.e., font and color), and button style contains attributes such as normal background color, highlighted color, label font style, corner radius, and more. Later in this article, we’ll discuss how we apply styles in greater detail.

Setting label with style and text example:

label.applyStyle(.sectionHeader, text: Strings.Localizable.title)

Step 2 — Break Down the Process

Force Dark Mode

The next step was to enable dark mode in the project to gain some initial insights regarding the scale of changes we were facing and better understand how we’d break this big epic into smaller tasks.

Break Down Into Small Standalone Tasks

It’s not trivial to execute a project of this magnitude on a production app. Accordingly, we had to establish some prerequisites first:

  • We had to make sure that our work could co-exist alongside other day-to-day tasks.
  • Each task on this project had to be as standalone as possible so that each developer could take one when available, execute it, test it, and merge it back.
  • The tasks had to be small, such that execution would not take more than one day of work.
  • The developer should be able to switch context, if needed.
  • Reduce the amount of simultaneous work in the same places in the project, in order to avoid conflicts during later stages and prevent refactoring the same reusable code twice due to a lack of communication between developers.

With those things in mind, we created a board that contained all major UI flows in the app. When available, each developer assigned himself to an item on this board. Then, he had to break down each item into those smaller standalone tasks. Each task represented a single view or view controller, and included all of its subviews, layouts, cells, etc.

After all of the tasks for one board item were completed, we marked it as ready for QA.

There was one developer whose job was to oversee the execution of those board items and decide what would go back to the development branch after testing.

The dark mode project Monday board

Source Control

Although opening a branch for any task sounds obvious, the process of separating this epic from other development code and merging back only the items that were tested is very important. This is to ensure that such a big project can co-exist with other day-to-day tasks and periodic version releases.

Our approach was to create a new dark mode parent branch, and out of it we created children branches for each board item, where a single commit to a child branch usually represents a single task (i.e., a view or view controller).

After all of the tasks on a child branch were completed and tested, it was merged back to the parent branch.
Before each new version release, we aligned our development branch with the new code from the dark mode parent branch. Keep in mind that all of the code on this branch was already tested.

Force Dark Mode 2 — The Return of the Light Mode

Our periodic alignment with the development branch meant that after some time, although our application did not support dark mode officially, a substantial amount of the UI on production was already “dark mode ready.” This process was transparent to users. We even took dark mode into consideration whenever we worked on a new feature.

On production, we disabled dark mode by setting the UIUserInterfaceStyle property list key to light; however, on our dark mode parent branch we removed it so that we could toggle between dark and light appearances while working on the UI.

As part of the testing process, before merging any item back to the development branch, we had to go back to light mode and make sure that we didn’t create any regressions to the current app appearance.

It was crucial to avoid overriding the user interface style property list key on the development branch so that we wouldn’t unintentionally release a half-baked dark mode version.

Tip!

When working on a simulator, you can hold shift + command + A to toggle appearance between dark and light.
You achieve the same behavior on a real device by adding a dark mode toggle to the control center. On your device, go to settings -> control center and add dark mode.

Testing

Both the QA testers and designers were involved in the testing process, but each team took a slightly different role:

  • QA Testers: Responsible for finding bugs and regressions to existing screens affected by the changes.
  • Designers: Review converted screens, spot mistakes, and identify places where we should reconsider the use of some palette colors for dark appearance. We had a Figma page dedicated to fixes and changes.
The design Figma QA page

Step 3 — Execution

As a rule of thumb, we decided that the only source of truth when it comes to design is the code. Rather than separate logic between the interface builder and their classes, we set (or override) every aspect of the design on the class itself.

Therefore, if we use IB, each view must have a layout in the file’s owner class unless it’s transparent or not visible to the user.
On the view or view controller lifecycle methods (viewDidLoad\ awakeFromNib\ layoutSubviews), we set design elements for each view. Even if a view might change its UI traits later on in its lifecycle, we set a default appearance.

Needless to say, everything should be set up here, including background color of the main view, tint colors, or bar font styles.

Xcode debug view hierarchy is a great tool for testing our work. It makes it easier to spot views that might be difficult to find at first glance, and in many instances it is excellent for finding views that are not visible on flat hierarchy, but that will appear on animations or reloads and look buggy if not assigned with the right colors.

debug view hierarchy

Tip!

While views can harness the power of UIColor dynamic appearances, and change automatically when system appearance changes, layers are still bound to CGColors. This means that we have to do some manual work to change their appearance.
Luckily for us, there is an easy solution for this in the form of the UITraitEnvironment protocol.
Just override the traitCollectionDidChange function on your view and set the desired custom behavior.

Step 4 (Optional) — Set Appearance in the App

There is an option to let users change the application appearance while disregarding their device system settings.

By overriding overrideUserInterfaceStyle, we can set a particular style to a single view, small view hierarchy, view controller, or entire window and its view controllers and presentations.

Even though it might sound odd, to our surprise, approximately 40% percent of our users preferred this option. In fact, half of them actually used dark mode on their device, but preferred our app on light appearance.

Conclusion

Dark mode feels like a must-have feature for any application these days, and on paper, we have enough tools to support different appearances while creating our UI. However, in real life, big projects tend to contain tons of legacy code, outdated UI, and sometimes even support for old OS versions that delays transitions to new technologies.

All of these elements can make large-scale changes like this feel terrifying to the average developer (or team). That’s why we decided to approach this problem differently.

First, we laid out the foundation for a good, scalable design system that helped improve communication with the designers.
Then, we broke everything down into small standalone tasks that each developer could take on between other assignments.
Most importantly, a good strategy for merging work back to the main code base in a manner that should be transparent to application users.

Accordingly, when the time came, and the application felt ready, all we had to do was flip a switch, or change a property list key, and voila, dark mode!

Fiverr is hiring. Learn more about us here.

--

--