iOS 13 Dark mode at BlaBlaCar, a developer’s story

Victor Carmouze
BlaBlaCar
Published in
8 min readOct 21, 2020

When a design system goes dark, from Charlotte Martin and Victor Carmouze.

Why wouldn’t we be able to travel at night?

At BlaBlaCar, as developers, we have the opportunity to push some features we would like to have every quarter. This is why, in June, we decided with support from our UX team to develop a Dark Mode version of our app, as it’s a great improvement for users.

The app feels fresh, protects your eyes at night and even saves your battery life. It’s also nice for us developers to work with the latest APIs on a new, widely used, iOS feature.

Since iOS 13, Apple provides new APIs which allow applications to support this new system-wide appearance. It provides great tools to manage colors, icons and illustrations. Our UX team, which started working on this project of designing a new dark appearance for the BlaBlaCar app, provided us with great specifications and support.

In this article, we’ll explain how we implemented it, what was easy and what was more complicated about the process. We will first present our design system and how we migrated it. Then, we’ll show that we only had a few changes to make to our main application before discussing what we would like to improve in the future.

Migrating a design system to Dark Mode

Throughout the company, we use a design system on our apps (iOS, web, Android). It is a set of design rules and components created and maintained by our UX team to help keep consistency through all of our products.

On iOS, we implemented it as a framework containing visual components following those rules. Each screen in the BlaBlaCar app is built only using those components, as you would build Lego from bricks.

Every one of them is initialized with an injected style guide object. It contains a set of colors we call a palette and a set of fonts that we must use to create our components.

Here are examples of components contained in our design system:

Some components from our design system

These components allow us to build screens:

A screen built only with components from our design system library

When we update a component, all the views containing it will be updated accordingly. Sounds great when we want to update our library to support dark mode!

Basically, we just need to update our library components and the whole app will go dark, avoiding us the work of checking every view controller.

Of course, we need to be careful when updating a component because it will affect every screen it’s used in. To avoid that, we use snapshot testing. We take reference screenshots of all screens and compare them to updated ones each time we build our design system. This prevents regressions, which can be easily missed when working on UI code.

If we exclude legacy screens (which do not use our design system) and exceptions, it took us less than two weeks to create a Dark-mode version for our design system.

Let’s see how we did that.

First of all, how do we enable dark mode?

When building an app with Xcode 11 dark mode is automatically enabled in your app. If you are not supporting it yet, you should override UIUserInterfaceStyle to light in your main plist file.

When the appearance is updated by the user in the settings, traitCollectionDidChange method is called in a view or a viewController. If we need to do any update, we can override this method

Colors

When we are talking about a new interface style, the first thing you might think about is colors. Texts, backgrounds and icons should get new colors.

We use a palette of colors in our design system. Each component’s colors rely only on this set of colors. As we have a specific palette, we can’t use any system color which already supports different appearance.

First things first, we need a new palette for the dark theme.

Corresponding light and dark colors

Once we’ve got our corresponding colors we then can use new iOS 13 dynamics colors. We need to create a new UIColor which will be initialized with two colors. The light one and the matching dark one.

We rely on a new UIColor initialiser:

UIColor { (UITraitCollection: UITraitCollection) -> UIColor }

It allows us to provide a UIColor for each user's appearance style. We must check it’s dark mode availability using:

if #available(iOS 13, *)

Each of our components is now able to dynamically switch between our palette’s light and dark colors. For example, the color blue will be defined like this:

static let blue = color(lightHex: 0x00AFF5, darkHex: 0x0E94C9)

CGColors

One of the first issues we have to deal with is CGColors, which doesn’t take advantage of new dynamic UIColors.

It means that our CGColors will be initialized with the right, light or dark, color, defined in our palette, but if we dynamically update system appearance, layers CGColors won’t be updated, which can cause… edge cases:

CGColors are not updated when the appearance changes, check the Continue with Apple button
This is the comportment we’re looking for

Hopefully, we have the traitCollectionDidChange method when a system appearance is updated.

When appearance changes, the method above is called. It allows resetting components CGColors. We simply need to call a dedicated method to set our UIColors or CGColors.

To get the right color from our dynamic UIColors, our setupColors method need another step.

We need to call resolveColor method on UIColor. ResolveColor will return the version of the current color that results from the specified appearance (which is contained in your traitCollection).

Assets

In light and dark mode, we may want to have:

  • The same asset no matter what the appearance is
  • A dedicated asset for each appearance

In case we want to use the same asset, the only thing to check is the background: it needs to be transparent and not white.

And when we want to use a different asset, in Appearances property our Asset Catalog, we set “Any, Dark”. Then we can add a new asset for the dark mode:

As for colors, our components are now able to dynamically alternate between light and dark assets.

Exceptions

When changing colors, not all components looked as expected by our amazing UX team. So, our designers added some exceptions to a few views.

An exception is when the dark appearance color does not conform to the white appearance color we defined in our palette.

Component exception

Here, our component has a green color background (0x054752) in light mode :

If we followed the specification by applying the corresponding dark color to the background, the component would look like this:

Designers thought that it would be better with a grey background (0x323839) :

To handle that case, we have to create an exception. We need to create a new pair of UIColors, which won’t conform to our main color palette.

Those new pairs of colors are our exceptions.

static let _greenToDarkGrey = color(lightHex: 0x054752, darkHex: 0x323839)

Then, by setting the background color of this view, it was done. This allowed us to handle exceptions for our components in a very easy way.

Fullscreen exception

We wanted our full screen to be exactly the same in light and dark mode instead of having a corresponding color:

In this case, instead of adding an exception on each component, we can override the interface style in init or viewDidLoad:

We force the view to use the light version of our color palette.

Testing dark mode

Finally, as our design system is a very important part of our codebase, we want to test all our components and icons. As explained above, some icons are different in light and dark mode. In order to test dark mode, let’s create a test to check those updated icons:

This test is going through all our icons and checks that none has been modified using the verifyInCanvas method which compares it to a reference screenshot.

We force dark mode with:

imageView.overrideUserInterfaceStyle = .dark

How did it affect the main app?

As explained before, most of the changes had to be developed in our design system. We actually almost have nothing to change in our main application. The most frequent issue is that when the background view isn’t set, it’s totally black instead of having the dark background of our application. So don’t forget to set this property!

Legacy screens

Let’s commute to the wonderful world of legacy code

As we mentioned previously, we still have legacy screens which do not conform to our design system. It is for example the case in our profile screen:

Our profile legacy screen

For this kind of screen, we have to deal with a lot of small bugs which are almost always the same:

  • The background is set to .white instead of .background of our palette
  • The background of our tableview cells is not set
  • The color is set in the storyboard: we have to reset these colors in the code.

What’s next

Our implementation relies on assets embedded in the application’s IPA. But what about assets returned by our backend services?

In order to display assets retrieved from our APIs corresponding to the user’s chosen appearance (light or dark), we decided to send it in our requests’ headers.

The backend will return the asset corresponding to the right appearance and those assets will be reusable on all platforms (iOS, android and web).

Another thing we will need to improve is our design system tests. Today, each component is tested through UI Screenshots testing. Currently only light components -except icons - are UI tested. We’ll need to generate a new bunch of dark screen screenshots and test them alongside.

Special thanks to Emilie, UX and iOS team at BlaBlaCar which help us on this project and review this article.

--

--