WWDC this year brought to light a lot of amazing features Apple has been working on lately. One of these features, and maybe one of the most expected, was Dark Mode support.

As Apple mentioned in their keynote, Dark Mode is easy to implement and will bring a whole new look to your application.

Though easy to implement, support for Dark Mode doesn’t come for free. In this post we will look at what is needed from us, the developers, to join the dark side.

Before we dive into specifics about Dark Mode implementation, let us look at Apples Human Interface Guidelines and see what they say about Dark Mode.

The main rules to follow presented in Apple’s guidelines are the following:

  • Focus on your content.
  • Test your design in both light and dark appearances.
  • Ensure that your content remains comfortably legible in Dark Mode when you adjust the contrast and transparency accessibility settings.

Semantic Colors

To support Dark Mode the main thing we have to consider in our iOS applications is the way we handle and implement colors. Traditionally each piece of our UI would have a color - created by providing a single RGB value - assigned, but in order to implement the dark appearance for our UI, we will need to change this approach and instead start using what is referred to as “Semantic Colors”.

Making a color semantic simply means that instead of referring to the color directly (red, blue, yellow), we refer to the color by the role is has (background, text, important). This means that we shouldn’t directly assign RGB colors to our views, but instead create the colors in a separate part of our application before we assign them. For example you will declare and assign a primary background color in a similar fashion.

// create an UIColor extension to declare our colors
extension UIColor {
	static let primaryBackgroundColor = UIColor.black
}
// assign the primaryBackgroundColor to our backgroundView in our UIViewController or UIView
backgroundView.backgroundColor = .primaryBackgroundColor

Semantic colors have more benefits than just support for Dark Mode, they will also empower you to easily manage your colors.

In Xcode 11 Apple has provided us with several semantic colors. For example UIColor.systemBackground will be white for a light appearance and dark for a dark appearance.

We can also create our own custom colors. To do so we need to navigate to our Assets catalog (Assets.xcassets) and add a New Color Set. Now that we have created a new color, we need to select the Attribute inspector in the top right corner and select values Any, Dark for our color Appearances.

When a color has multiple appearance values, we call the color “Dynamic Color”.

Design Levels

For dark appearance support Apple has introduced a two level design. This contains:

  • base level: which is used when the view fills the whole screen
  • elevated level: which is used when the view is in a separate layer above the base

To identify the level of the view, you can access the UITraitCollection.userInterfaceLevel value associated with the view.

This will allow you to create a more stylished UI by providing different values for the same color, depending on the userInterfaceLevel.

Dynamic Colors

To obtain the final color, dynamic colors get resolved using a UITraitCollection. For system defined colors this happens automatically, but, we can also declare our own dynamic colors programatically! “How?” you might wonder. Well lets see together.

I suggest that you create your own colors in an extension to UIColor. This will allow you to easily access your colors whenever a UIColor is needed, by reducing the need to call UIColor.myColor and instead just provide the color .myColor.

Now lets look at what it takes to create our own custom dynamic color.

import UIKit

extension UIColor {
    
    // create a dynamic color to use as a background color in our application
    static let dynamicBackgroundColor = UIColor { (traitCollection: UITraitCollection) -> UIColor in
    	 // resolve the color by using traitCollection
        switch traitCollection.userInterfaceStyle {
        case .dark:
            return .black
        case .light, .unspecified:
            return .white
        @unknown default: // may have additional userInterfaceStyles in the future
            return .white
        }
    }
    
}

This will allow you to assign your background color to your view with the following code view.backgroundColor = .dynamicBackgroundColor. As you can see nothing changed in the way we assign color, but how does it know to return the proper color if no UITraitCollection is specified? This is handled in the background by UIKit and it uses the newly introduced UITraitCollection.current.

Resolving Dynamic Colors

UIKit sets the current UITraitCollection for the view during the draw() method. As well, when the appearance changes, UIKit will automatically call setNeedsDisplay on your view, which will cause the view to be redrawn with the proper UITraitCollection value.

It is important to note that the value associated with UITraitCollection.current is not guaranteed to be correct if called outside of the following functions, in which UIKit sets the value for the UITraitCollection.

  • UIView - draw()
    • layoutSubviews()
    • traitCollectionDidChange()
    • tintColorDidChange()
  • UIViewController - viewWillLayoutSubviews()
    • viewDidLayoutSubviews()
    • traitCollectionDidChange()
  • UIPresentationController - containerViewWillLayoutSubviews()
    • containerViewDidLayoutSubviews()
    • traitCollectionDidChange()

To use dynamic colors outside of these methods you might need to manage the UITratCollection. This is needed when working with lower level classes such as CALayer, CGColor etc.

Lets see below how we can use dynamic colors when assigning a borderColor property to our view’s CALayer.

  let layer = CALayer()
  // get the current traitCollection used for our view
  let traitCollection = view.traitCollection
  traitCollection.performAsCurrent {
  	   // assign a dynamic borderColor called borderColor
      layer.borderColor = UIColor.borderColor.cgColor
  }

When performAsCurrent is called on a traitCollection it makes the traitCollection become the current traitCollection whereafter it runs the code inside the closure. This means that our borderColor is resolved accordingly.

Now that we have resolved our dynamic colors when creating our views, we need to be aware that there can be times when we will need to resolve the dynamic colors again. This usually happens when the current traitCollection changes. To listen to traitCollection changes we can override override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) in both our UIView’s and UIViewController’s. Though when overriding this function we should be aware that not all traitCollection changes will mean that we need to resolve our colors again. This will only be needed when the user interface appearance changes. We can check if the change is a change in apparance by using calling traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection).

  override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        guard traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) else { return }
        
        // Resolve Dynamic Colors here
    }

Resolving Dynamic Images

So far we have discovered the concept of “Dynamic Colors”. Similarly an UIImage can become a “Dynamic Image”.

Dynamic Images are automatically resolved by UIImageView but if we need to resolve our UIImage independently we can do so by accessing the imageAsset property on our UIImage.

let image = UIImage(named: "nodesLogoImage")
let asset = image?.imageAsset
// get the appropriate image by resolving the dynamic image
let resolvedImage = asset?.image(with: traitCollection)

Final notes

Trait Collections play an important role when implementing Dark Mode. One important thing worth remembering is that there isn’t a single app wide traitCollection value but instead they cascade through the app’s hierarchy.

Supporting Dark Mode for your application will definitely make your users happy, and as we saw above, it is not a really complex task to do so. Just remember to pay attention when creating and assigning your dynamic colors.

Hope to see you next time!

References:

Cover Photo by Lubo Minar on Unsplash.

ios swift dark darkmode ios13 iphone

Author

Andrei Hogea

iOS Developer

iOS developer based in Copenhagen, Denmark. Always curious

You may also like

An overview of watchOS complications

Complications are bits of watchOS application that can be put on the user’s watch face and offer quick information, simply by looking at his/her Apple Watch, without opening the accompanying watchOS/iOS app. The Apple Watch ships with some default complications: Calendar, Weather, Activity, Battery (it shows only the watch’s battery...

iOS
Nordic.js 2019

The 9th version of the Nordic.js conference took place at Magasin 9 near the harbor of Stockholm. The whole frontend team was invited, and three of us were able to attend (Rasmus, Themi and Jonas). It was our first trip to a conference as a team, and it was nice...

frontend afk