After a long wait and anticipation, at last iOS 13 supports native Dark Mode. Users would be able to choose to allow a system wide dark appearance that will be supported in all official apps. As we will see, Apple has also made it simple for developers to add dark mode support with minimum effort.
iOS 13 Dark Mode support changes:
-
Status bar style : default, darkcontent , lightcontent
-
Activity indicator : medium, large, Depreciated (gray, white, whitelarge)
-
UILabel, UITextField, UITextView : Use Semantic Colors or Custom Colors for light and dark mode
-
AttributedString : requires providing foregroundColor
-
For Embedded web content : Declare supported color schemes with color-scheme, Use prefers-color-scheme media query for custom colors and image
-
Images : Dark mode images
-
Images Tint Color : Dark mode tint color
Let’s make a start!
If you have already done this, then it’s great and now we will discuss what you can do more to make interface better. So let’s start on “How to implement Dark Mode”.
Step 1: Colors
At the end, actually our app is to throw colors and if we are getting colors right, then we are almost ready to launch our app in dark mode.
System Colors (Dynamic)
Before iOS 13, UI Color was offering only few simple colors like black, red, white and yellow. Now, due to iOS 13 we don’t need to use these colors because these colors are static which means they can’t adopt tint changes and remain the same as they were before.
Some colors are dynamic i.e. (systemRed) and they can adopt lighter colors in dark mode and darker colors in light mode rather than remaining same as static. In iOS 13+, it’s better to use the new system color which will respect the user’s color scheme preference:
label.textColor = UIColor.label
Compatibility:
What if, instead of if #available, there was a way to abstract the color choice down one level, so we could do something like this?
label.textColor = ColorCompatibility.label
Once we cover those, we can use their red/green/blue/alpha components to create the implementation of Color Compatibility that we want:
Enum ColorCompatibility { Static var label: UIColor { If #available(iOS 13, *) { return .label } return UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) } static var secondaryLabel: UIColor { if #available(iOS 13, *) { return .secondaryLabel } return UIColor(red: 0.9215686274509803, green: 0.9215686274509803, blue: 0.9607843137254902, alpha: 0.6) } // ... 34 more definitions: full code in the link at the bottom }
We can then use Color Compatibility to set any colors we need.
Custom Colors (Dynamic): The Assets Catalog
In custom colors, chances of errors are more for you and design team. Apple team has already worked on dynamic colors for our ease. In Xcode 11, we can also define variant with color set.
If we want to design our own custom color, for that, first we have to go into Assets Catalog and open the attribute inspector, and set its appearance from None to Any, Dark.
Programmatically:
In iOS 13, a new UIColor initializer was introduced:
init(dynamicProvider: @escaping (UITraitCollection) ->UIColor)
You can customize your own color, based on the userInterfaceStyle property from UITraitCollection:
extension UIColor { static func myColorForDark() -> UIColor { if #available(iOS 13, *) { return UIColor.init { (trait) -> UIColor in return trait.userInterfaceStyle == .dark ? UIColor.darkGray : UIColor.orange } } else { return UIColor.blue } } }
Don’t forget to enable high contrast as well.
As you can see in the below picture, we have defined four different variants for one color. Again, I strongly suggest using System and Semantic Colors as much as possible:
Step 2: Images
SF Symbols:
Apple introduced SF Symbols at WWDC19. SF Symbols is a huge collection of glyphs (over 1500!) that are available for developers to use in their own apps.
Apple itself uses SF Symbols in each stock app like Reminders, News, Maps and others.
You can fetch any of them by using the new API UIImage(systemName:)
_ = UIImage(systemName: "star.fill")
Like SF Symbols, template images are monochrome images that are defined in our Xcode assets catalog by selecting “render as” Template Image in the Attribute Inspector. By using them, you get several advantages. To name one, you gain dark mode for free.
let myGlyphImage = UIImage(named: "myGlyph")
let myGlyphImageView = UIImageView(image: myGlyphImage)
myGlyphImageView.tintColor = .systemBlue
Other Images:
For all other kind of images that are not template images or symbols, such as photos and more, we can follow the same steps as for custom colors: set their appearance to any, Dark in the asset catalog and drop a new variant for each appearance.
We can see in the below image how to set image for dark and light mode, we also see that how we can set simulator far dark mode:
As you can see in the above picture how images have a look in dark mode and light mode.
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 myDarkImage = UIImage(named: "SunAndMoon")
let asset = myDarkImage?.imageAsset
Let resolvedImage = asset?.image(with: traitCollection)
Detecting Dark Mode:
There could be some cases in which you want to detect appearance changes programmatically and change your user interface accordingly
func ViewChanges() { if(traitCollection.userInterfaceStyle == .dark) { MoodShift.text = "Night Mode" } else { MoodShift.text = "Light Mode" } } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) let userInterfaceStyle = traitCollection.userInterfaceStyle// Either .unspecified, .light, or .dark // Update your user interface based on the appearance print(userInterfaceStyle) ViewChanges() }
Specific Screens:
To override the user interface style, just override this variable in the top view or view controller and it will propagate down to subviews:
import UIKit class CheckViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Always adopt a Light interface style. overrideUserInterfaceStyle = .light // Do any additional setup after loading the view. } }
Step 3: Drawing Attributed Text
If we are using Attributed Text, then we must have to use .foregroundColor property. Otherwise it set to black color and uses UIColor.label for correct results. As you can see in the pictures below that what happens if we don’t use .foregroundColor property.
When drawing attributed text, if not specified, the .foregroundColor property is set to .black:
set it to a proper color instead (e.g. UIColor.label).
let textDraw = "This text is an attributed string." let attributes: [NSAttributedString.Key: AnyObject] = [ .font: UIFont.preferredFont(forTextStyle: .title3), .foregroundColor: UIColor.label] textDraw.draw(at: CGPoint.zero, withAttributes: attributes)
A Deeper Look:
If your app completely relies on storyboards for the UI, then congratulations!
You’re now set to fully support Dark Mode.
Not all of us are this lucky, if you’re not among these people , read on.
Behind The Scenes: Draw Time
iOS picks the right tint/image of our dynamic colors/images at draw time: but when is “draw time” exactly
As you know, our views can become invalid at some point in their lifetime:
-
Maybe the user has rotated the screen.
-
Maybe a UIView needs to add a new element in the interface, etc.
You’re always guaranteed to have iOS pick the right tint/material/image when you’re inside any of the following methods:
-
UIView
-
draw()
-
layoutSubviews()
-
traitCollectionDidChange()
-
tintColorDidChange()
-
-
UIViewController
-
viewWillLayoutSubviews()
-
viewDidLayoutSubviews()
-
traitCollectionDidChange()
-
-
UIPresentationController
-
containerViewWillLayoutSubviews()
-
containerViewDidLayoutSubviews()
-
containerViewDidLayoutSubviews()
-
Dark Mode In CALayers:
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.
let layer = CALayer() // get the current traitCollection used for our view let traitCollection = view.traitCollection traitCollection.performAsCurrent { layer.borderColor = (UIColor.self as! CGColor) } // Do any additional setup after loading the view.
Roadmap to start implementing Dark Mode:
-
Download and install Xcode 11 beta
-
Build and Run your app with dark mode enabled
-
Fix the obvious “mistakes” spotted
-
Add dark variants to all your assets
-
Make sure to set the foreground key when drawing attributed text
-
Move all your appearance logic in the “Draw time” functions
-
Adapt Dark Mode one screen at a time:
-
Start from the .xibs files
-
Move to storyboards
-
Move to code
-
Repeat for all screens
-