Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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
}

参考リンク

gonsee
カレンダーシェアアプリ「TimeTree」のiOSアプリを開発しています。個人では「陣痛時計」、「ごみの日アラーム」というアプリをリリースしています。Perfumeエバンジェリスト。
https://simplebeep.net/iphone-apps
jubileeworks
「毎日に、新しい"なくてはならない"を創る」を経営理念に掲げ、共有カレンダーサービス TimeTree の開発・運営をしています。
https://timetreeapp.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした