Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS 13.0 - Best approach for supporting Dark Mode and also support iOS 11 & 12

So I've posted on Apple Developer Forums, but haven't gotten a reply yet.

Background:

iOS 13 has introduced Dark Mode and a number of System Colors with predefined Light and Dark variants: (https://developer.apple.com/videos/play/wwdc2019/214/)

These colors can be used in the storyboard directly as named colors. They've also been added as static colors to the UIColor class: (https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors)

However, static colors added to UIColor are not available in code in iOS 11 and 12. This makes its tricky to use them as all references to the new System Colors must be wrapped in an availability check: Availability checks for UIColors

It also raises the question: on iOS 11 and 12, what will the System colors resolve to when used directly in the Storyboard? In our testing they seem to resolve to the Light variant, though we haven't tested all of them.


Current approach:

This is the approach we are leaning towards. We will add all colors to our Colors.xcassets file for older iOS version support, and through our CustomColors Enum perform a single version check and mapping so the correct UIColor system colors is returned depending on the iOS version. Once we drop support for iOS 11 and 12 we will remove the respective colors from Colors.xcassets as we will only be using the System Colors instead. We will also refactor all our storyboards to use the new System Colors.

Custom color enum

The drawbacks of this approach are:

  • If we want to use system colors directly in our code after we drop support for iOS 11 and 12 (UIColor.label, UIColor.systemBackground, etc), it could be quite a large refactor to get rid of all the enum references
  • Because we will be using System Colors in our storyboard, we must ensure that our Colors.xcassets equivalents use the same color code
  • This bug: (UIColor(named:) always returns nil on iOS 11.0-11.2) - if its not fixed then this approach is unusable (EDIT: This bug is fixed in XCode 11 GM seed 2 11A420a)
  • As with all Asset Catalogs, using magic strings to access items in the catalog makes it easy for developers to make a mistake and get nil instead of the asset (the color in this case). This could result in difficult-to-pick-up bugs if we don't test every single screen, forcing us to write the crashIfAllColorsNotDefined() method. using an enum does mitigate this risk as the magic strings are only stored/used in one place.

Other approaches: (How do I easily support light and dark mode with a custom color used in my app?)


Question:

What are some other approaches one could use to support Dark Mode with iOS 13 by using the new System colors, while still supporting iOS 11 and 12? And is it safe to use the new System Colors in Storyboards on older iOS versions?

like image 759
Antag Avatar asked Aug 30 '19 00:08

Antag


People also ask

Does iOS 13 support dark mode?

In iOS 13.0 and later, users can choose to adopt a dark appearance called Dark Mode. In Dark Mode, apps and system use a darker colors for all screens, controls and views.

How do I make iOS app compatible with dark mode?

You configure custom color assets using Xcode's asset editor. Add a Color Set asset to your project and configure the appearance variants you want to modify. Use the Any Appearance variant to specify the color value to use on older systems that do not support Dark Mode.

How do I enable dark mode in Xcode?

Pro tip: You can switch the simulator between light and dark mode without going back to Xcode. In the simulator, choose Features ▸ Toggle Appearance — Shift-Command-A — to switch between them.


1 Answers

A combination of Enum and UIColor Extension was the way to go in the end. There are two 'parts' to the custom colors - your app's special colors and duplicate apple colors.

Some of the new colors Apple released are only available in iOS13 or later (systemBackground, opaqueSeparator, secondaryLabel, etc). If you want to use these right away then you'll have to create them as custom colors. This is a concern because it increases future technical debt, as these colors would have to be refactored once iOS13 becomes your minimum supported version. This is especially difficult to refactor in Storyboards.

The way this solution is set up, the UIColors extension can be easily modified to return the official apple colors at a later stage. You should only set duplicate apple colors programmatically - don't use them directly in Storyboards.

In code:

self.backgroundColor = .red1
self.layer.borderColor = UIColor.successGreen1.cgColor

Colors Enum:

// Enum for all custom colors
private enum CustomColors : String, CaseIterable {
    case red1 = "red1"
    case red2 = "red2"
    case blue1 = "blue1"
    case blue2 = "blue2"
    case successGreen1 = "successGreen1"
    case warningOrange1 = "warningOrange1"

    //----------------------------------------------------------------------
    // MARK: - Apple colors
    //----------------------------------------------------------------------

    // Duplicates for new apple colors only available in iOS 13
    case opaqueSeparator = "customOpaqueSeparator"
    case systemBackground = "customSystemBackground"
    case systemGroupedBackground = "customSystemGroupedBackground"
    case secondarySystemGroupedBackground = "customSecondarySystemGroupedBackground"
    case secondaryLabel = "customSecondaryLabel"
    case systemGray2 = "customSystemGray2"
}

UIColor extension:

// Extension on UIColor for all custom (and unsupported) colors available
extension UIColor {

    //----------------------------------------------------------------------
    // MARK: - Apple colors with #available(iOS 13.0, *) check
    //----------------------------------------------------------------------

    // These can all be removed when iOS13 becomes your minimum supported platform.
    // Or just return the correct apple-defined color instead.

    /// Opaque Seperator color
    static var customOpaqueSeparator: UIColor {
        if #available(iOS 13.0, *) {
            return UIColor.opaqueSeparator
        } else {
            return UIColor(named: CustomColors.opaqueSeparator.rawValue)!
        }
    }

    /// System Background color
    static var customSystemBackground: UIColor {
        if #available(iOS 13.0, *) {
            return UIColor.systemBackground
        } else {
            return UIColor(named: CustomColors.systemBackground.rawValue)!
        }
    }

    /// System Grouped Background color
    static var customSystemGroupedBackground: UIColor {
        if #available(iOS 13.0, *) {
            return UIColor.systemGroupedBackground
        } else {
            return UIColor(named: CustomColors.systemGroupedBackground.rawValue)!
        }
    }

    // more

    //----------------------------------------------------------------------
    // MARK: - My App Custom Colors
    //----------------------------------------------------------------------

    /// Red 1 color
    static var red1: UIColor {
        return UIColor(named: CustomColors.red1.rawValue)!
    }

    /// Red 2 color
    static var red2: UIColor {
        return UIColor(named: CustomColors.red2.rawValue)!
    }

    /// Success Green 1 color
    static var successGreen1: UIColor {
        return UIColor(named: CustomColors.successGreen1.rawValue)!
    }

    // more

    //----------------------------------------------------------------------
    // MARK: - Crash If Not Defined check
    //----------------------------------------------------------------------

    // Call UIColor.crashIfCustomColorsNotDefined() in AppDelegate.didFinishLaunchingWithOptions. If your application 
    // has unit tests, perhaps ensure that all colors exist via unit tests instead.

    /// Iterates through CustomColors enum and check that each color exists as a named color.
    /// Crashes if any don't exist.
    /// This is done because UIColor(named:) returns an optionl. This is bad - 
    /// it means that our code could crash on a particular screen, but only at runtime. If we don't coincidently test that screen
    /// during testing phase, then customers could suffer unexpected behavior.
    static func crashIfCustomColorsNotDefined() {
        CustomColors.allCases.forEach {
           guard UIColor(named: $0.rawValue) != nil else {
            Logger.log("Custom Colors - Color not defined: " + $0.rawValue)
            fatalError()
           }
       }
    }
}

In Storyboards:

Pick custom colors directly, except for the duplicate apple colors.

Colors.xcassets: Colors.xcassets

like image 168
Antag Avatar answered Sep 26 '22 13:09

Antag