iOS13適配深色模式(Dark Mode)

iOS 13
  • 原文博客地址: iOS13適配深色模式(Dark Mode)
  • 好像大概也許是一年前, Mac OS系統(tǒng)發(fā)布了深色模式外觀, 看著挺刺激, 時(shí)至今日用著也還挺爽的
  • 終于, 隨著iPhone11等新手機(jī)的發(fā)售, iOS 13系統(tǒng)也正式發(fā)布了, 伴隨著手機(jī)版的深色模式也出現(xiàn)在了大眾視野
  • 我們這些iOS程序猿也有事情做了, 原有項(xiàng)目適配iOS13系統(tǒng), 適配Dark Mode深色模式
  • 雖然現(xiàn)在并沒(méi)有要求強(qiáng)制適配Dark Mode, 但是DarK適配卻也迫在眉睫

Apps on iOS 13 are expected to support dark mode Use system colors and materials Create your own dynamic colors and images Leverage flexible infrastructure

獲取當(dāng)前模式

提供兩種方式設(shè)置手機(jī)當(dāng)前外觀模式

  • 設(shè)置 --> 顯示與亮度
  • 控制中心, 長(zhǎng)按亮度調(diào)節(jié)按鈕

獲取當(dāng)前模式

我們需要選獲取到當(dāng)前出于什么模式, 在根據(jù)不同的模式進(jìn)行適配, iOS 13中新增了獲取當(dāng)前模式的API

Swift

// 獲取當(dāng)前模式
let currentMode = UITraitCollection.current.userInterfaceStyle
if (currentMode == .dark) {
    print("深色模式")
} else if (currentMode == .light) {
    print("淺色模式")
} else {
    print("未知模式")
}

open var userInterfaceStyle: UIUserInterfaceStyle { get } 

// 所有模式
public enum UIUserInterfaceStyle : Int {
    // 未指明的
    case unspecified
    // 淺色模式
    case light
    // 深色模式
    case dark
}

OC語(yǔ)言

if (@available(iOS 13.0, *)) {
    UIUserInterfaceStyle mode = UITraitCollection.currentTraitCollection.userInterfaceStyle;
    if (mode == UIUserInterfaceStyleDark) {
        NSLog(@"深色模式");
    } else if (mode == UIUserInterfaceStyleLight) {
        NSLog(@"淺色模式");
    } else {
        NSLog(@"未知模式");
    }
}

// 各種枚舉值
typedef NS_ENUM(NSInteger, UIUserInterfaceStyle) {
    UIUserInterfaceStyleUnspecified,
    UIUserInterfaceStyleLight,
    UIUserInterfaceStyleDark,
} API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);

監(jiān)聽(tīng)系統(tǒng)模式的變化

iOS13系統(tǒng)中, UIViewController遵循了兩個(gè)協(xié)議: UITraitEnvironmentUIContentContainer協(xié)議

UITraitEnvironment協(xié)議中, 為我們提供了一個(gè)監(jiān)聽(tīng)當(dāng)前模式變化的方法

@protocol UITraitEnvironment <NSObject>
// 當(dāng)前模式
@property (nonatomic, readonly) UITraitCollection *traitCollection API_AVAILABLE(ios(8.0));

// 重寫該方法監(jiān)聽(tīng)模式的改變
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection API_AVAILABLE(ios(8.0));
@end
public protocol UITraitEnvironment : NSObjectProtocol {
    // 當(dāng)前模式
    @available(iOS 8.0, *)
    var traitCollection: UITraitCollection { get }

    // 重寫該方法監(jiān)聽(tīng)模式的改變
    @available(iOS 8.0, *)
    func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
}


// 使用方法
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    
    // 每次模式改變的時(shí)候, 這里都會(huì)執(zhí)行
    print("模式改變了")
}

顏色相關(guān)適配

  • 不同模式的適配主要涉及顏色和圖片兩個(gè)方面的適配
  • 其中顏色適配, 包括相關(guān)背景色和字體顏色
  • 當(dāng)系統(tǒng)模式切換的時(shí)候, 我們不需要如何操作, 系統(tǒng)會(huì)自動(dòng)渲染頁(yè)面, 只需要做好不同模式的顏色和圖片即可

UIColor

  • iOS13之前UIColor只能表示一種顏色,從iOS13開(kāi)始UIColor是一個(gè)動(dòng)態(tài)的顏色,在不同模式下可以分別代表不同的顏色
  • 下面是iOS13系統(tǒng)提供的動(dòng)態(tài)顏色種類, 使用以下顏色值, 在模式切換時(shí), 則不需要做特殊處理
@interface UIColor (UIColorSystemColors)
#pragma mark System colors

@property (class, nonatomic, readonly) UIColor *systemRedColor          API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemGreenColor        API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemBlueColor         API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemOrangeColor       API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemYellowColor       API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemPinkColor         API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemPurpleColor       API_AVAILABLE(ios(9.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemTealColor         API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemIndigoColor       API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
// 灰色種類, 在Light模式下, systemGray6Color更趨向于白色
@property (class, nonatomic, readonly) UIColor *systemGrayColor         API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemGray2Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray3Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray4Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray5Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray6Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);

#pragma mark Foreground colors
@property (class, nonatomic, readonly) UIColor *labelColor              API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *secondaryLabelColor     API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *tertiaryLabelColor      API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *quaternaryLabelColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
// 系統(tǒng)鏈接的前景色
@property (class, nonatomic, readonly) UIColor *linkColor               API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
// 占位文字的顏色
@property (class, nonatomic, readonly) UIColor *placeholderTextColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
// 邊框或者分割線的顏色
@property (class, nonatomic, readonly) UIColor *separatorColor          API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *opaqueSeparatorColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);

#pragma mark Background colors
@property (class, nonatomic, readonly) UIColor *systemBackgroundColor                   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemBackgroundColor          API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemBackgroundColor           API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGroupedBackgroundColor            API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemGroupedBackgroundColor   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemGroupedBackgroundColor    API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);

#pragma mark Fill colors
@property (class, nonatomic, readonly) UIColor *systemFillColor                         API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemFillColor                API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemFillColor                 API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *quaternarySystemFillColor               API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);

#pragma mark Other colors
// 這兩個(gè)是非動(dòng)態(tài)顏色值
@property(class, nonatomic, readonly) UIColor *lightTextColor API_UNAVAILABLE(tvos);    // for a dark background
@property(class, nonatomic, readonly) UIColor *darkTextColor API_UNAVAILABLE(tvos);     // for a light background

@property(class, nonatomic, readonly) UIColor *groupTableViewBackgroundColor API_DEPRECATED_WITH_REPLACEMENT("systemGroupedBackgroundColor", ios(2.0, 13.0), tvos(13.0, 13.0));
@property(class, nonatomic, readonly) UIColor *viewFlipsideBackgroundColor API_DEPRECATED("", ios(2.0, 7.0)) API_UNAVAILABLE(tvos);
@property(class, nonatomic, readonly) UIColor *scrollViewTexturedBackgroundColor API_DEPRECATED("", ios(3.2, 7.0)) API_UNAVAILABLE(tvos);
@property(class, nonatomic, readonly) UIColor *underPageBackgroundColor API_DEPRECATED("", ios(5.0, 7.0)) API_UNAVAILABLE(tvos);

@end
  • 上面系統(tǒng)提供的這些顏色種類, 根本不能滿足我們正常開(kāi)發(fā)的需要, 大部分的顏色值也都是自定義
  • 系統(tǒng)也為我們提供了創(chuàng)建自定義顏色的方法
@available(iOS 13.0, *)
public init(dynamicProvider: @escaping (UITraitCollection) -> UIColor)

在OC中

+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *traitCollection))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
- (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *traitCollection))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
  • 上面的方法接受一個(gè)閉包(block)
  • 當(dāng)系統(tǒng)在LightModeDarkMode之間相互切換時(shí)就會(huì)自動(dòng)觸發(fā)此回調(diào)
  • 回調(diào)返回一個(gè)UITraitCollection, 可根據(jù)改對(duì)象判斷是那種模式
fileprivate func getColor() -> UIColor {
    return UIColor { (collection) -> UIColor in
        if (collection.userInterfaceStyle == .dark) {
            return UIColor.red
        }
        return UIColor.green
    }
}

除了上述兩個(gè)方法之外, UIColor還增加了一個(gè)實(shí)例方法

// 通過(guò)當(dāng)前traitCollection得到對(duì)應(yīng)UIColor
@available(iOS 13.0, *)
open func resolvedColor(with traitCollection: UITraitCollection) -> UIColor

CGColor

  • UIColor只是設(shè)置背景色和文字顏色的類, 可以動(dòng)態(tài)的設(shè)置
  • 可是如果是需要設(shè)置類似邊框顏色等屬性時(shí), 又該如何處理呢
  • 設(shè)置上述邊框?qū)傩? 需要用到CGColor類, 但是在iOS13CGColor并不是動(dòng)態(tài)顏色值, 只能表示一種顏色
  • 在監(jiān)聽(tīng)模式改變的方法中traitCollectionDidChange, 根據(jù)不同的模式進(jìn)行處理
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    
    // 每次模式改變的時(shí)候, 這里都會(huì)執(zhí)行
    if (previousTraitCollection?.userInterfaceStyle == .dark) {
        redView.layer.borderColor = UIColor.red.cgColor
    } else {
        redView.layer.borderColor = UIColor.green.cgColor
    }
}

圖片適配

  • iOS中, 圖片基本都是放在Assets.xcassets里面, 所以圖片的適配, 我們就相對(duì)麻煩一些了
  • 正常情況下都是下面這中處理方式
image
  • 需要適配不同模式的情況下, 需要兩套不同的圖片, 并做如下設(shè)置
  • 在設(shè)置Appearances時(shí), 我們選擇Any, Dark就可以了(只需要適配深色模式和非深色模式)
image

適配相關(guān)

當(dāng)前頁(yè)面模式

  • 原項(xiàng)目中如果沒(méi)有適配Dark Mode, 當(dāng)你切換到Dark Mode后, 你可能會(huì)發(fā)現(xiàn), 有些部分頁(yè)面的顏色自動(dòng)適配了
  • 未設(shè)置過(guò)背景顏色或者文字顏色的組件, 在Dark Mode模式下, 就是黑色的
  • 這里我們就需要真對(duì)該單獨(dú)App強(qiáng)制設(shè)置成Light Mode模式
// 設(shè)置改屬性, 只會(huì)影響當(dāng)前的視圖, 不會(huì)影響前面的controller和后續(xù)present的controller
@available(iOS 13.0, *)
open var overrideUserInterfaceStyle: UIUserInterfaceStyle

// 使用示例
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    
    // 每次模式改變的時(shí)候, 這里都會(huì)執(zhí)行
    if (previousTraitCollection?.userInterfaceStyle == .dark) {
        // 在Dark模式下, 強(qiáng)制改成Light模式
        overrideUserInterfaceStyle = .light
    }
}

強(qiáng)制項(xiàng)目的顯示模式

  • 上面這種方式只能針對(duì)某一個(gè)頁(yè)面修改, 如果需要對(duì)整個(gè)項(xiàng)目禁用Dark模式
  • 可以通過(guò)修改windowoverrideUserInterfaceStyle屬性
  • Xcode11創(chuàng)建的項(xiàng)目中, windowAppDelegate移到SceneDelegate中, 添加下面這段代碼, 就會(huì)做到全局修改顯示模式
let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.overrideUserInterfaceStyle = .light

在之前的項(xiàng)目中, 可以在AppDelegate設(shè)置如下代碼

window.overrideUserInterfaceStyle = .light

我創(chuàng)建的簡(jiǎn)單項(xiàng)目, 上述代碼的確會(huì)強(qiáng)制改變當(dāng)前的模式, 但是狀態(tài)欄的顯示不會(huì)被修改, 不知道是不是漏了什么

終極方案

  • 需要在info.plist文件中添加User Interface Style配置, 并設(shè)置為Light
<key>UIUserInterfaceStyle</key>
<string>Light</string>

問(wèn)題又來(lái)了, 即使做了上面的修改, 在React Native中, 狀態(tài)欄的依然是根據(jù)不同的模式顯示不同的顏色, 該如何處理嘞?

Status Bar更新

iOS13中蘋果對(duì)于Status Bar也做了部分修改, 在iOS13之前

public enum UIStatusBarStyle : Int {
    case `default` // 默認(rèn)文字黑色

    @available(iOS 7.0, *)
    case lightContent // 文字白色
}

iOS13開(kāi)始UIStatusBarStyle一共有三種狀態(tài)

public enum UIStatusBarStyle : Int {
    case `default` // 自動(dòng)選擇黑色或白色

    @available(iOS 7.0, *)
    case lightContent // 文字白色
    
    @available(iOS 13.0, *)
    case darkContent // 文字黑色
}

React Native的代碼中, 設(shè)置狀態(tài)欄的顏色為黑色, 代碼如下

<StatusBar barStyle={'dark-content'} />
  • 上面這段代碼在iOS13系統(tǒng)的手機(jī)中是無(wú)效的
  • 雖然上面的代碼中設(shè)置了dark-content模式, 但是在iOS原生代碼中dark-content實(shí)際是UIStatusBarStyleDefault
  • 在文件RCTStatusBarManager.m
RCT_ENUM_CONVERTER(UIStatusBarStyle, (@{
  @"default": @(UIStatusBarStyleDefault),
  @"light-content": @(UIStatusBarStyleLightContent),
  @"dark-content": @(UIStatusBarStyleDefault),
}), UIStatusBarStyleDefault, integerValue);

修改上面代碼即可

@"dark-content": @(@available(iOS 13.0, *) ? UIStatusBarStyleDarkContent : UIStatusBarStyleDefault),

iOS13 其他更新

蘋果登錄

Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.

  • 如果APP支持三方登陸(Facbook、Google、微信、QQ、支付寶等),就必須支持蘋果登陸,且要放前邊
  • 至于Apple登錄按鈕的樣式, 建議支持使用Apple提供的按鈕樣式,已經(jīng)適配各類設(shè)備, 可參考Sign In with Apple

LaunchImage

即將被廢棄的LaunchImage

  • iOS 8的時(shí)候,蘋果就引入了LaunchScreen,我們可以設(shè)置LaunchScreen來(lái)作為啟動(dòng)頁(yè)。
  • 現(xiàn)在你還可以使用LaunchImage來(lái)設(shè)置啟動(dòng)圖, 但是隨著蘋果設(shè)備尺寸越來(lái)越多, 適配顯然相對(duì)麻煩一些
  • 使用LaunchScreen的話,情況會(huì)變的很簡(jiǎn)單,LaunchScreen是支持AutoLayoutSizeClass的,所以適配各種屏幕都不在話下。
  • ??從2020年4月開(kāi)始,所有App將必須提供LaunchScreen,而LaunchImage即將退出歷史舞臺(tái)

UIWebView

'UIWebView' was deprecated in iOS 12.0: No longer supported; please adopt WKWebView.

iOS 13開(kāi)始也不再支持UIWebView控件了, 盡快替換成WKWebView

@available(iOS, introduced: 2.0, deprecated: 12.0, message: "No longer supported; please adopt WKWebView.")
open class UIWebView : UIView, NSCoding, UIScrollViewDelegate { }

歡迎您掃一掃下面的微信公眾號(hào),訂閱我的博客!

微信公眾號(hào)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容