This post was made with contributions from @kaydacode
Introducing dark mode to an existing and very large enterprise application had some burning concerns. Looking at the heavily integrated colors and views spanning across programmatic, reusable components, and storyboards (even xibs!), it really was like looking at a mountain of work with no clear path forward. What laid ahead, what caveats would we face, what could we do from the start to keep this rollout manageable? Well, I’m happy to report after a few dedicated weeks, we launched dark mode and it was a massive success. In this article, we’ll layout the key steps we took to help reach success, as well as the caveats and lessons learned along the way.
Setup
First thing was to create a dedicated branch for the dark mode changes. We had some rules in place to ensure optimal success with not only converting colors, but also testing and keeping in sync with the main development branch. The dark mode branch was ONLY to have color changes. Any refactoring work or functionality changes to support the color swaps had to happen on develop and be tested appropriately at that time. The second key piece of setup was inspired by a brief exposure to design systems. Instead of having direct UIColors set throughout the app, we would be changing our mindset to “tokens”. A token in this context represents how a color is used rather than what the color is. More on tokens in the next section.
Development
Once our dark mode branch was setup and we were ready to start development, the first step was to flush out the obvious tokens. To reduce complexity, we defined a function that would handle passing the appropriate color based off the current trait collection.
static func setColor(darkMode: UIColor, lightMode: UIColor) -> UIColor {
return UIColor { (traitCollection: UITraitCollection) -> UIColor in
switch traitCollection.userInterfaceStyle {
case .dark:
return darkMode
case .light, .unspecified:
return lightMode
@unknown default:
return lightMode
}
}
}
Then we were able to define our tokens based off usage rather than color.
static var neutralBackgroundColor: UIColor {
return setColor(darkModeColor: .customBlack, lightModeColor: .customWhite)
}
With this step, we were able to have generic tokens that helped enforce the app into consistent design patterns. We defined tokens such as 'neutralTextColor', 'buttonBackgroundColor', 'neutralBackgroundColor' and so on.
The most difficult part was going through the app and updating all of the necessary colors. To update, we took a screen by screen approach and carefully traversed through each flow. It was challenging because some colors we got for free with inheritance, some view colors were set in the storyboard, and some programmatically. Going forward, we moved all colors to be set in code. Programmatic colors allowed us to override anything coming from the storyboards (as those weren’t branded colors anyway), and be completely transparent about what the expected outcome is within the code. Taking a moment to callout that reusable views were the MVP here. The changes were consistent and automatically flowed through the app as each one was updated. It also highlighted any views that were similar enough to be changed to the reusable one, but fell through the cracks over the years. Once the code was at a stable state, we started moving our color palette to private. This forced colors to be only available through the tokens. This move not only ensured future development would use the tokens over the colors, but that Xcode would throw an error with any color still attempting to be accessed.
While developing, we ran into quite a few accessibility challenges that we were not prepared for. It wasn’t always as easy as moving white colors to black and so on. Likewise, within branding guidelines, dark alternative colors were not provided, so we did a lot of experimenting and back and forth with a handful of designers to get it right. WEBAIM has a contrast checker tool that was a lifeline during development. Highly recommend checking anything with a questionable contrast. Some problems we identified resulted in a complete color change in light and dark mode to remedy the low visibility in dark mode. Also emphasizing utilizing design resources that are available to you. Conversations opened networking that lead to other team wins also!
The last piece of development that was key to this success was enabling an appearance toggle where the user can change the appearance of the app for the app alone, and retain that setting. This was built out for any user who may experience low visibility in one or the other and wanted to retain a selection. Think of it as a usability safety net. We used a UISegmentedControl that allowed “Light”, “Dark” or “System” selections to enforce that trait used locally going forward. System would read from whatever the user has selected in the devices System Settings. By default "System" was selected.
Shipping/ Testing
After all the development work, make sure all the work gets tested by Software Quality Engineers. During testing, we need to test all the happy paths. By doing so, we will be confident about all the important user flows. The funnel/screen approach should be very helpful in this process. You may discover a handful of views/screens that got missed. Have a stab at all those views right away.
And remember, pushing this work to production as early as possible is the key. There are so many benefits of doing this
- It will help reduce the merge conflicts while maintaining the dark mode feature branch
- Other developers might be working alongside this work, so the sooner you merge this work to the main branch, the easier it gets for all the developers to follow new colors/standards
- The users of the app will get the dark mode support early :)
Lessons
FYI, this was not the first attempt of working towards this goal. It was the second attempt by a newly joined team member.
The first attempt was not successful because
- Not everyone from the team was on board with this goal.
- The benefits of doing this work were not clear to all the developers.
- The lead developer didn't present this idea with very minimal changes as a Proof of Concept.
- The initial Pull Request for the POC was not easy to review because of a handful of storyboard changes.
On the other hand, the second attempt was successful because this time lead developer got everyone on the same page about the benefits of doing this work and how easy it is to do if done correctly.
Here are the lessons after this successful attempt
- Create a new feature branch for dark-mode support work, and don't merge it until you are fully done. Meaning, don't use something like feature flagging and all. It would be very difficult to manage.
- Always keep this feature branch in sync with the main branch.
- If you have used the storyboards to set the colors to the UI elements, this is a great time to set the colors programmatically instead. Especially, go with the route of using newly created color tokens instead of using UIColor directly.
- There will be some extra effort to make sure the new work/feature is dark-mode supported. Have the whole team on board so that it will be easier to have dark mode support for new work/feature.
- Have a regular sync-up with the designers to follow the correct guidelines. You might get some feedback, and it's good to have that in advance instead of at the very end.
We hope this guide was helpful to your dark mode adventures and look forward to more apps supporting this great feature. Let us know in the comments how it's going!
Top comments (0)