Last year, Dark Mode was one of the most exciting features of macOS Mojave. After waiting for what felt like an eternity, Dark Mode was finally announced at WWDC this year as a system-wide appearance in iOS 13. During WWDC, a group of iOS engineers and designers in our team banded together to plot out what it would take to adopt dark mode. It’s been such a tough journey, months of work and collaboration, much of code refactoring, to bring dark mode to our app. As such, we wanted to take some time to share how we approached and some of the obstacles that we encountered along the way.
Apple’s done a great job shaping how dark mode works in iOS 13. UIKit provides flexible and convenient APIs as well as excellent documentation. Most of the heavy lifting in selecting appropriate colors or images when transition between light and dark are done by UIKit. But after hours of experiments in these new APIs, we found some limitations.
- Only light and dark are supported. So if we stick with Apple’s APIs, it will be impossible to have other custom themes.
- New APIs are only available in iOS 13 or later. It means that we’d have to handle the transition of UI elements between light and dark ourselves in older iOS versions.
- Dynamic color is an UIKit concept. So lower-level classes, like
CGColor, will not understand it.
- Backwards-compatible: Since most developers in the team and our build systems are all still using Xcode 10 so we still have to write wrappers around UIKit APIs that are compatible with Xcode 10 and iOS 12.
“Standing on the shoulders of giants” and trying to stick with Apple’s APIs are great ideas but after hours of discussion, we decided to go with the approach of writing our own APIs that can resolve all of limitations above.
Building our own APIs
When building new APIs, we followed these key principles:
- Lightweight: Has minimum overhead that achieves similar performance to UIKit APIs. Transition between themes must be smooth and animatable.
- Compatibility: Supports all iOS versions.
- Ease of use: Appropriate colors and images should be selected automatically without any changes in code logic.
- Familiarity: Newcomers to our iOS codebase who are familiar with UIKit APIs will feel right at home.
- Extensible: New themes can be easily added.
The first step in building new APIs is defining some handy primitives and concepts.
- Dynamic colors: Colors that change in response to appearance changes.
- Dynamic images: Images that change in response to appearance changes.
- Dynamic value providers: Similar to dynamic colors or images, but they can provide dynamic values for any custom data type.
- Semantic colors: Named dynamic colors that are used for a particular purpose. They’re named according to their function rather than appearance. For example “primary background color” or “secondary label color”.
Next we collaborated with our design systems team to define a set of color names as well as its hex values. After that, we built a tool to generate Objective-C code from our color palettes. The generated code for named dynamic colors looks like.
At this point, we have a set of
AZColor objects. You may notice that we’re passing a color name into every initializer above. Each dynamic color object has a different name which will be used to resolve the color information at runtime.
This is how we resolve a named dynamic color.
Now that we have
AZColor in the app, we can use it where we need to use dynamic colors. Our developers can freely use this without worrying about runtime crashes regardless of iOS version compatibility.
Another challenge we embraced was to get the work in selecting appropriate colors done automatically when transition between themes without any changes in code logic.
To do that, we decided to create an alternative getter/setter for every color properties.
For example, in
UIView’s category, we have these new properties. In which,
az_tintColoris the replacement for
az_backgroundColoris the replacement for
In the setter, the given dynamic color will be registered in the receiver as an applier. Every time when the theme is changed, the applier is responsible for resolving the color information at that moment. After that the receiver’s property will be updated to the appropriate color as well.
Here’s an implementation of one of these getters/setters.
Once we had how the applier worked, we continued to write some macros that allow us to easily synthesize getter/setter for new properties without code duplication.
This is what ours looks like:
From here, we had this pattern defined, the final step is to add more as they were needed. The set that we ended up with is:
- AZColor as shown above.
- AZImage dynamic images that automatically adapt to theme changes.
- AZDynamicValueProvider provide dynamic values for any custom data type. For example “NSAttributedString” or “NSDictionary”.
- Synthesizer macros: to easily synthesize getter/setter for dynamic color, dynamic image, dynamic keyboard appearance, dynamic attributed string…
Here’re some examples of how we combine them together in the app.
The only one thing that we have to do when user change their theme preference is:
Another obstacle we encountered was to prevent asset duplication when adopting dark mode.
If you have multiple themes in your app, asset duplication can be a bad influence on your app size as well as maintainability. In order to resolve this we collaborated with our design team and decided to just use images that look good in both light and dark modes.
Some tricky things that we used for images are:
- Gradient color can help maintain good contrast of image on both light and dark backgrounds.
- Create images with some adjustment in alpha channel. Translucent images can be well-combined with background color to produce new blended colors that look great in both light and dark modes.
- Colorize images with dynamic tint colors. Template images will look great in both appearance modes when you use dynamic colors to tint them.
We know that it’s been such a risk-return tradeoff between sticking with Apple’s APIs and writing our own one. However, by building our own APIs, we can easily add more amazing themes in the future, thereby satisfying our users by allowing them to choose their favorite appearance rather than forcing them to have just light or dark.
This week’s update to Zalo includes full support for dark mode in all iOS versions. And this approach has played an important role in the development process. Of course the actual implementation still wasn’t easy, our engineering and design teams had a deep dive into every part of the app. Admittedly, we probably missed some. Hope you all enjoy this update!
I hope this short tutorial will be useful to you. Let’s make your app look right at home on iOS 13. Cheers!
If you want to learn more about how these APIs work, feel free to leave your feedback below or you can always reach me on LinkedIn. See you in next post!