Antag
Antag

Reputation: 230

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:

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?

Upvotes: 12

Views: 2144

Answers (1)

Antag
Antag

Reputation: 230

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

Upvotes: 1

Related Questions