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

Ant*_*tag 11 uicolor ios uistoryboard swift ios13

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: 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.

自定义颜色枚举

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?)

Edit: This bug could make almost any code-based approach unusable:(https://openradar.appspot.com/radar?id=4995701796765696)

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?

Ant*_*tag 1

Enum 和 UIColor Extension 的组合是最终的选择。自定义颜色有两个“部分” - 您的应用程序的特殊颜色和重复的苹果颜色。

Apple 发布的一些新颜色仅在 iOS13 或更高版本中可用(systemBackground、opaqueSeparator、 secondaryLabel 等)。如果您想立即使用它们,那么您必须将它们创建为自定义颜色。这是一个问题,因为它会增加未来的技术债务,因为一旦 iOS13 成为最低支持版本,这些颜色就必须重构。这在故事板中重构尤其困难。

按照此解决方案的设置方式,可以轻松修改 UIColors 扩展以在稍后阶段返回官方苹果颜色。您应该仅以编程方式设置重复的苹果颜色 - 不要直接在情节提要中使用它们。

在代码中:

self.backgroundColor = .red1
self.layer.borderColor = UIColor.successGreen1.cgColor
Run Code Online (Sandbox Code Playgroud)

颜色枚举:

// 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"
}
Run Code Online (Sandbox Code Playgroud)

UI颜色扩展:

// 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()
           }
       }
    }
}
Run Code Online (Sandbox Code Playgroud)

在故事板中:

直接选择自定义颜色,除了重复的苹果色。

颜色.xcassets: 颜色.xcassets