iOS 13からのダークモード対応について、具体的な実装方法、注意点、現行バージョンと並行開発するコツなどをまとめてみました。
定義済みのダイナミックシステムカラーについて
これまで色の指定は UIColor.white
のように静的な色を使っていましたが、iOS 13からはライト・ダークのモードに応じて変化するダイナミックカラーという仕組みが導入されました。
システムで定義されたダイナミックカラーを UIColor
から利用することができます。label
, systemBackground
のように用途に応じた名前で定義されるものはセマンティックカラーとも呼ばれます。システムと同じ色を使うのが適当なケースではこれらを利用しましょう。
ダークモードかどうかを判定する
ダークモードかどうかは UITraitCollection
のプロパティ userInterfaceStyle
で判定できます。 UIView
や UIViewController
は traitCollection
プロパティから 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」を選択するとダークモード用の色を定義できます。
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
}