Posted at

iOS 13からのダークモード対応のコツ

iOS 13からのダークモード対応について、具体的な実装方法、注意点、現行バージョンと並行開発するコツなどをまとめてみました。


定義済みのダイナミックシステムカラーについて

これまで色の指定は UIColor.white のように静的な色を使っていましたが、iOS 13からはライト・ダークのモードに応じて変化するダイナミックカラーという仕組みが導入されました。

システムで定義されたダイナミックカラーを UIColor から利用することができます。label, systemBackground のように用途に応じた名前で定義されるものはセマンティックカラーとも呼ばれます。システムと同じ色を使うのが適当なケースではこれらを利用しましょう。


ダークモードかどうかを判定する

ダークモードかどうかは UITraitCollection のプロパティ userInterfaceStyle で判定できます。 UIViewUIViewControllertraitCollection プロパティから UITraitCollection のインスタンスにアクセスできます。

また、以下のようなエクステンションを書いておくと便利だと思います。 availability の判定をあちこちに書かずに済ませるためにも有効です。

extension UITraitCollection {

public static var isDarkMode: Bool {
if #available(iOS 13, *), current.userInterfaceStyle == .dark {
return true
}
return false
}

}

ただし、以下の項目で説明しますが、実際にこのコードを利用するシーンはあまりないと思います。


ダイナミックカラーを生成する

ビューの生成時などにダークモードかどうかを判定して静的な色をセットしても、アプリ動作中にモードが切り替わったときに追随できなくなってしまいます。

モード切り替え時に追随できるようにするにはダイナミックカラーを利用します。 UIColor にクロージャーを渡して、クロージャーの中でモードごとの色を返すようにすることで、動的な色を作れるイニシャライザが新たに導入されました。 UIColor が直接利用できる場面ではこれを使うのがいいでしょう。

 let dynamicColor = UIColor { (traitCollection: UITraitCollection) -> UIColor in

if traitCollection.userInterfaceStyle == .dark {
return .black
} else {
return .white
}
}

こちらも以下のようなメソッドを UIColor のエクステンションに書いておくと便利です。

/// ライト/ダーク用の色を受け取ってDynamic Colorを作って返す

public class func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
if #available(iOS 13, *) {
return UIColor { (traitCollection) -> UIColor in
if traitCollection.userInterfaceStyle == .dark {
return dark
} else {
return light
}
}
}
return light
}


カスタムのセマンティックカラーを定義する

アプリ独自のカラーセットをセマンティックカラーとして UIColor のエクステンションに定義して、実際に色を指定する箇所ではこちらを利用するのがいいと思います。ダークモード導入以前からこのように定義しているアプリも多いのではないでしょうか。

ダイナミックカラーを返すようにすることで、モード切り替え時も自動的に色が置き換わるようになります。

/// 背景色

public static var background: UIColor {
return dynamicColor(
light: .white
dark: .black
)
}

/// メインのテキストの色
public static var text: UIColor {
return dynamicColor(
light: UIColor(hex: 0x212121),
dark: UIColor(hex: 0xF5F5F5)
)
}

init(hex:)も独自のエクステンションです。

deployment targetがiOS 11以上であればAsset CatalogでColor Setを定義することができます(画像はMacアプリの例ですがiOSも同様です)。右ペインのAppearacesから「Any, Dark」を選択するとダークモード用の色を定義できます。

Asset CatalogでColor Setを定義する

Supporting Dark Mode in Your Interface より

Storyboardから指定したり、コードから init(named:) で指定すればダイナミックカラーとして利用できます。


ダイナミックカラーを直接使えないケース

色指定に CGColor を使わないといけないケースや、 withAlphaComponent(_:) でalphaをかけているケースなどは、ダイナミックカラーを直接利用できないので注意が必要です。

このようなケースでは、モード切り替え時に呼ばれるメソッドの中で色の指定をしておくことで、適切にアップデートできます。モード切り替え時に呼ばれるメソッドは以下の通りです。



  • UIView


    • traitCollectionDidChange(_:)

    • layoutSubviews()

    • draw(_:)

    • updateConstraints()

    • tintColorDidChange()




  • UIViewController


    • traitCollectionDidChange(_:)

    • updateViewConstraints()

    • viewWillLayoutSubviews()

    • viewDidLayoutSubviews()




  • UIPresentationController


    • traitCollectionDidChange(_:)

    • containerViewWillLayoutSubviews()

    • containerViewDidLayoutSubviews()



CGColor で色を指定する例

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

super.traitCollectionDidChange(previousTraitCollection)
textLabel.layer.borderColor = UIColor.border.cgColor
}


現行バージョン向けと並行開発する

iOS 13のリリースまでまだ時間がありますので、それまでに新規の画面開発や既存画面の大幅な改修などが入ることもあると思います。その場合、今のうちにダークモード対応のことを考慮しておきたいので、現行バージョンのXcode 10用ブランチとiOS 13対応をしているXcode 11用ブランチで同一のコードが動く状態にしました。

例えば上に挙げたdynamic colorを返すメソッドは、以下のように #if swift(>=5.1) を挟むことによってどちらの環境でもビルドして動作させることができます。

public class func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {

#if swift(>=5.1)
if #available(iOS 13, *) {
return UIColor { (traitCollection) -> UIColor in
if traitCollection.userInterfaceStyle == .dark {
return dark
} else {
return light
}
}
}
#endif
return light
}


参考リンク