Implementing Darkish Mode in iOS 13. How we carried out darkish mode at… | by Tim Johnsen
One of the most exciting announcements at WWDC this year was the introduction of platform-wide dark mode in iOS 13. During WWDC, a group of avid iOS engineers and designers from Instagram’s design systems team came together to find out what one Acquisition required is dark mode in our app. This week’s update on Instagram has full support for iOS dark mode. This required months of work and collaboration between numerous design and engineering teams in the company. Because of this, we wanted to take some time to share how we approached Dark Mode and some of the obstacles we encountered along the way.
Apple has done a great job of making dark mode work in iOS 13. Most of the heavy lifting is done by UIKit on your behalf. For this reason, one of the most important principles in building dark mode support in our app was that we should “stand on the shoulders of giants” and try to stick with Apple’s APIs as much as possible. This is beneficial for several reasons.
- user friendliness – UIKit does most of the work in choosing appropriate colors and transitioning between light and dark mode. If we were to write our own APIs, we would have to do it ourselves.
- Maintainability – Apple manages the APIs so we don’t have to do this. All of the wrappers we have can ultimately be switched to use UIKit APIs once our minimum supported OS version is iOS 13.
- familiarity – Newbies to Instagram’s iOS codebase who are familiar with how UIKit works in dark mode will feel right at home.
That being said, we didn’t use UIKit’s APIs alone as most of the developers in the company and our build systems are still using Xcode 10 and the introduction of iOS 13 APIs would lead to build breaks. We took the approach of writing thin wrappers around UIKit APIs that are compatible with Xcode 10 and iOS 12.
Another principle we followed was to introduce as few APIs as possible and only when needed. The main reason for this was to reduce complexity for product teams using dark mode: it is harder to misunderstand or abuse APIs when there are fewer of them. We started with just wrappers around dynamic colors and a semantic color palette created by our design systems team, and then rolled out additional APIs over time as the demand within the company increased. In order to raise awareness and ensure constant acceptance, we have announced a new API in an internal working group in dark mode and documented it on an internal wiki page for the project.
Apple defines some handy basics and concepts for dark mode, and since we’ve chosen to build on their APIs, we’ve adopted those too. We covered them at a high level.
- Dynamic colors – Colors that change in response to changes in light / dark mode. May also change in response to height and accessibility settings.
- Dynamic images – Similar to dynamic colors, these are images that change in response to changes in light / dark mode.
- Semantic colors – Named dynamic colors that serve a specific purpose. For example “destructive button color” or “link text color”.
- Height level – Things that are modally displayed in dark mode change the colors slightly to demonstrate that they are one level above the underlying user interface. For the most part, this concept did not exist in the light mode, as dark dimming layers are sufficient to distinguish between the modal layers displayed on top of one another.
One of the key APIs iOS 13 is introducing for dark mode support is the UIColor + colorWithDynamicProvider: method, which is used to generate colors that automatically adapt to dark mode. This was the very first API we wanted to wrap up for use on Instagram, and it’s still one of our most widely used dark mode APIs. We will implement it as a case study in creating a backward compatible wrapper.
The first step in creating such an API is to define a macro that we can use to compile code for people who are still using stable versions of Xcode under certain conditions. This is what ours looks like:
Next we declare a wrapper function. Our dynamic color wrapper looks like this:
Within this function, we use our macro to ensure that developers using older versions of Xcode can still compile. We’re also introducing a runtime check to keep the app working normally on older versions of iOS. If both tests are passed, we simply call the iOS 13 + colorWithDynamicProvider: API, otherwise we fall back on the light mode variant.
You may find that instead of a UITraitCollection, we are passing an IGTraitCollection to the block of IGColorWithDynamicProvider. We introduced IGTraitCollection as a structure that contains the values userInterfaceStyle and userInterfaceLevel of UITraitCollection as isLight and isElevated, respectively, since these properties are only available if they are linked to newer iOS versions. More on that later.
Now that we have IGColorWithDynamicProvider we can use it anywhere in the app that we need to use dynamic colors. Developers are free to use this without worrying about build errors or runtime crashes, regardless of which version of Xcode they or their colleagues are using. Instagram had a semantic color palette in the past that was introduced in our 2016 redesign and we worked with our design systems team to update all of the colors in it and support dark mode using IGColorWithDynamicProvider. Here is an example of one of those colors.
After defining this pattern for wrapping the UIKit API, we added more as needed. The set we ended up with is:
- IGColorWithDynamicProvider as shown here
- IGImageWithDynamicProvider for creating “dynamic images” that automatically adapt to dark mode.
- IGActivityIndicator features to create activity indicators with styles that work in light mode, dark mode, and older versions of iOS.
- IGSetOverrideUserInterfaceStyle for forcing views or view controllers into certain interface styles.
- IGSetOverrideElevationLevel for enforcing view controls at certain elevation levels.
Small side note: towards the end of our introduction to Dark Mode, we found that implementing dynamic colors had an impact on equality as a new instance of UIColor was returned each time and the only thing that was comparable to each was the block passed around this To fix this, we made minor changes to our API to create individual instances of each semantic color so that they are comparable. For example, if you use your semantic colors dispatch_once-ing or use asset catalog-based colors and + colorNamed :, comparable colors will be produced if your app is sensitive to color equality.
One of the tough things about introducing technology into iOS betas is getting adequate test coverage. Convincing people to use Instagram’s internal build to install iOS 13 on their devices isn’t a good idea as it’s unstable and difficult to help set up, and even if we got people to use iOS 13, the builds we distribute internally were still largely tied to the iOS 12 SDK, so the changes won’t show up anyway.
I briefly touched on our IGTraitCollection wrapper for UITraitCollection, which came in handy in building dark mode. A clever test trick that this IGTraitCollection wrapper made possible for us is the so-called “Fake Dark Mode” – an internal setting that IGTraitCollection overwrites in order to become dark even in iOS 12! Nate Stedman, one of our iOS engineers in New York, developed this setting when we first worked on dark mode.
The “Fake Dark Mode” option of our internal menu and the Fake Dark Mode are executed in a build that is linked to the iOS 12 SDK.
Our API for generating IGTraitCollections from UITraitCollections looked like this.
Whereby _IGIsDarkModeDebugEnabled is supported by an NSUserDefaults flag for the fake dark mode. There are, of course, some limitations to hiding dark mode in iOS 12, in particular
- userInterfaceLevel is not available in iOS 12, so “heightened” dynamic colors are never displayed in the fake dark mode.
- Forcing certain styles via our -setOverrideInterfaceStyle: Wrapper has no effect in fake dark mode.
- UIKit components that use their default colors do not adapt to the fake dark mode in iOS 12 because they have no knowledge of dark mode.
With this addition to our dark mode wrappers, we were able to achieve much broader test coverage than we otherwise could have done.
Dark mode has been a coveted feature from us for some time.
A recent public Q&A with Adam Mosseri, head of Instagram
We’ve been a little reluctant to introduce dark mode in the past because it would have been a daunting undertaking, but the great tools Apple provides and their emphasis on dark mode in iOS 13 finally made it possible for us! Of course, the actual implementation still wasn’t easy, we’ve been working on it since WWDC and it required extensive design and engineering immersion in every part of the app (and admittedly we probably missed a few). This trip was worth it. In addition to the benefits of Dark Mode, such as: B. Reducing eye strain and saving batteries, our app looks like home on iOS 13!
Big thanks to Jeremy Lawrence, Nate Stedman, Cameron Roth, Ryan Olson, Garrett Olinger, Paula Guzman, Héctor Ramos, Aaron Pang, and numerous others who contributed to our efforts to introduce Dark Mode. Dark mode is also available in Instagram for Android.
If you would like to learn more about this work or would like to join one of our engineering teams, please visit our careers page, follow us on Facebook or Twitter.