iOS 12以下でDark Modeに対応した地獄の話 #iosdc #a #b/dark_mode_iosdc_2019

249b3122eee454c0a818bfe7851418e4?s=47 fromkk
September 07, 2019

iOS 12以下でDark Modeに対応した地獄の話 #iosdc #a #b/dark_mode_iosdc_2019

# iOS 12以下でDark Modeに対応した地獄の話

TimersでiOSエンジニアをしている植岡和哉と申します。Twitter, Github, Qiitaなどは全てfromkkでいう名前で活動しています。
WWDC 2019でiOS 13向けにDark Modeが発表されました。
・Dark Modeは暗い背景に明るいテキストやコンテンツを表示するカラースキームの事を言います。カスタムカラーを利用する場合はコントラスト比4.5:1〜7:1を目指すと良いらしいです。
・目やバッテリーに優しいとされています。特に夜間に明るい画面は見たくないですね。
・iOS 13ではLight Mode、Dark Modeそれぞれに色の微調整が行われています。ちなみに色に名前や役割を付けてアプリ上で扱いやすくする事をセマンティックカラーと呼びます。
僕が個人的に開発している# Typeというアプリにダークモード機能を追加しました。設定画面からON/OFFするとアプリ全体の色が変更されます。

## Dark Modeを自分で実装する時の考え方

色を整理します。セマンティックカラーを意識してアプリ全体で利用する色を管理します。
UIからのイベントを受け取ります。
UIが変更された事を受け取ってアプリ全体で管理している色のモードを変更します。
モードが変更された事を検知してUIに反映させる
ColorSetというライブラリを開発しました。
https://github.com/fromkk/ColorSet
色の種類が変更された事を検知してUIに色を設定する事を簡素化出来るライブラリです。subscribeとbindというメソッドを使い分ければある程度柔軟に利用が可能かと思います。
よかったらスターください!
ここまでで対応方法は大体わかりましたね。実際のプロダクトに適用してみよう!と思ったのが地獄の始まりです。
(以下地獄絵図)

## 簡単に対処方法

Interface Builderやコードでの色の直接指定は禁止です。
動的変更に対応出来る様にしましょう。
Appearanceの指定で済むならそれが一番良いです。
画像が既存の背景色を前提としている場合は差し替えましょう。
差し替えが出来ない画像はイベントハンドリングで対応します。
アイコンの様な一色の画像はtintColorを設定すれば色が変わる様にします。
ステータスバーはモードが変更された時に更新する様にします。

# まとめ

・最初は色変更ぐらいで済むかと思っていたら、想像以上に対応しないといけない箇所が出てきて大変でした。
・特に元々背景色が白のアプリだと、背景色が指定済みかどうかが分りづらいです。。背景も意識して設定する事が大事だと思いました。
・縦や横画面、iPadでのSplit Viewなど様々な画面の確認が必要でした。全画面網羅のUIテストとスクリーンショットが求められます。

249b3122eee454c0a818bfe7851418e4?s=128

fromkk

September 07, 2019
Tweet

Transcript

  1. J04ҎԼͰ%BSL.PEFʹରԠͨ͠஍ࠈͷ࿩ !GSPNLLJ04%$BC !1

  2. 1SPpMF struct Profile { let name = "Kazuya Ueoka” let

    nickname = "͔ͬ͘Μ" let twitter = "@fromkk" let github = "fromkk" let qiita = "fromkk" let company = "Timers Inc." } • 2
  3. %BSL.PEF❓ 3

  4. %BSL.PEF 4

  5. 88%$ͰJ04޲͚ʹ%BSL.PEF͕ൃද !5

  6. %BSL.PEFͱ͸ʁ w ҉͍എܠʹ໌Δ͍ςΩετ΍ίϯςϯπΛදࣔ͢ΔΧϥʔεΩʔϜͷࣄ w ίϯτϥετൺΛ໨ࢦ͢   w ໨΍όοςϦʔʹ༏͍͠ͱ͞Ε͍ͯΔ w

    ໷ؒʹ໌Δ͍ը໘ݟͨ͘ͳ͍ w J04Ͱ͸Ϟʔυຖʹ֤৭ ηϚϯςΟοΫΧϥʔ ͷඍௐ੔͕ߦΘΕ ͍ͯΔ w 4'4ZNCPMT͸ͲͷϞʔυͰ΋Α͘ݟ͑Δ༷ʹͳ͍ͬͯΔ !6 IUUQTEFWFMPQFSBQQMFDPNEFTJHOIVNBOJOUFSGBDFHVJEFMJOFTBDDFTTJCJMJUZPWFSWJFXDPMPSBOEDPOUSBTU
  7. 5JQTίϯτϥετൺ !7 Ҿ༻IUUQTHJUIVCDPNFNBSMFZ$PMPS$POUSBTU

  8. !8

  9. J04ҎԼͰ΋%BSL.PEF͍ͨ͠ !9

  10. # Typeʹ΋Dark ModeೖΕͯΈͨ IUUQTUZQFNBSLEPXOBQQ

  11. %BSL.PEFΛ࣮૷͢Δҝͷߟ͑ํ !11

  12. ৭Λ੔ཧ͢Δ ৭ʹ໾ׂ΍ҙຯΛ෇༩͢Δ !12 protocol ColorSetType { static var theme: UIColor

    { get } static var action: UIColor { get } static var placeholder: UIColor { get } static var backgroundColor: UIColor { get } static var share: UIColor { get } static var archive: UIColor { get } static var error: UIColor { get } } %BSL.PEFΛ࣮૷͢Δҝͷߟ͑ํ
  13. 6*ͷΠϕϯτΛड͚औΔ !13 %BSL.PEFΛ࣮૷͢Δҝͷߟ͑ํ

  14. ΞϓϦશମͰ؅ཧ͍ͯ͠Δ৭Λมߋ͢Δ !14 class App { static var colorSet: ColorSetType =

    DefaultColorSet() } if modeSwitch.isOn { app.colorSet = DarkColorSet() } else { app.colorSet = DefaultColorSet() } %BSL.PEFΛ࣮૷͢Δҝͷߟ͑ํ
  15. Ϟʔυ͕มߋ͞ΕͨࣄΛݕ஌ͯ͠6*ʹ൓өͤ͞Δ !15 %BSL.PEFΛ࣮૷͢Δҝͷߟ͑ํ "QQ 7JFX" 7JFX# 7JFX$ 7JFX%

  16. Ϟʔυ͕มߋ͞ΕͨࣄΛݕ஌ͯ͠6*ʹ൓өͤ͞Δ !16 %BSL.PEFΛ࣮૷͢Δҝͷߟ͑ํ "QQ 7JFX" 7JFX# 7JFX$ 7JFX%

  17. IUUQTHJUIVCDPNGSPNLL $PMPS4FU ৭ͷछྨ͕มߋ͞ΕͨࣄΛײ஌ͯ͠ 6*ʹ৭Λઃఆ͢ΔࣄΛ؆ૉԽग़དྷΔ ϥΠϒϥϦ TVCTDSJCFͱCJOEΛ࢖͍෼͚Ε͹ ͋Δఔ౓ॊೈʹ !17 $PMPS4FU

  18. ελʔ͍ͩ͘͞ !18 $PMPS4FU IUUQTHJUIVCDPNGSPNLL $PMPS4FU ৭ͷछྨ͕มߋ͞ΕͨࣄΛײ஌ͯ͠ 6*ʹ৭Λઃఆ͢ΔࣄΛ؆ૉԽग़དྷΔ ϥΠϒϥϦ TVCTDSJCFͱCJOEΛ࢖͍෼͚Ε͹ ͋Δఔ౓ॊೈʹ

  19. ରԠํ๏͸େମ෼͔ͬͨ% !19

  20. ࣮ࡍͷϓϩμΫτʹద༻ͯ͠ΈΑ͏ !20

  21. ஍ࠈͷ࢝·Γ !21

  22. ݩʑͷը໘ !22

  23. ৭͚ͩΛมߋͯ͠Έͨ !23

  24. ·ͣ৭Λมߋͯ͠Έͨ ヤバそう !24

  25. ͱΓ͋͑ͣഎܠ৭ઃఆ !25

  26. ͱΓ͋͑ͣഎܠ৭ઃఆ なるほど? !26

  27. ৭ʑௐ੔ͯ͠ΈΔ !27

  28. ͍ͩͿྑ͘ͳ͖ͬͯͨʁ !28

  29. 残念でした !29

  30. 画像も対応しないと !30

  31. Α͠Α͠ !31

  32. ͜Ε͕ !32

  33. こうじゃ !33

  34. ΞΠίϯͷ༷ͳը૾͸UJOU$PMPSͷมߋͰରԠ !34

  35. ステータスバーも変えたい !35

  36. ͍͍͍͍ͧͧ !36

  37. ରॲํ๏ !37

  38. ରॲํ๏   view.backgroundColor = .white *#΍ίʔυͰͷ৭ͷ௚઀ࢦఆېࢭ) ⬇ TypeColorSet.backgroundColorLight .bind(to:

    view, keyPath: \.backgroundColor) .append(to: observationBag) !38
  39. ରॲํ๏ AppearanceͷઃఆͰࡁΉ৔߹͸Ұׅઃఆ͕खͬऔΓૣ͍ TypeColorSet.backgroundColorLight .bind(to: UITableViewCell.appearance(), keyPath: \.backgroundColor) .append(to: observationBag) !39

  40. ରॲํ๏ എܠ͕ന΍طଘͷ৭Λલఏͱ͍ͯ͠Δը૾͕͋Ε͹ࠩ͠ସ͑Δ !40

  41. ରॲํ๏ !41 TypeColorSet.theme.subscribe { [weak button] _ in let colorSet

    = UIColor.currentColorSet() switch colorSet.identifier { case .default: button?.setImage(#imageLiteral(resourceName: “bt_add_light"), for: .normal) case .dark: button?.setImage(#imageLiteral(resourceName: "bt_add_dark"), for: .normal) } }.append(to: observationBag) ࠩ͠ସ͑ग़དྷͳ͍ը૾͸ΠϕϯτϋϯυϦϯάͰରԠ
  42. ରॲํ๏ let imageView = UIImageView(image: UIImage(named: "image")) ⬇ let imageView

    = UIImageView(image: UIImage(named: “image")? .withRenderingMode(.alwaysTemplate)) TypeColorSet.theme.bind(
 to: imageView, keyPath: \.tintColor
 ).append(to: observationBag) ΞΠίϯͷ༷ͳҰ৭ͷը૾͸tintColorΛઃఆ͢Ε͹৭͕มΘΔ༷ʹରॲ !42
  43. ରॲํ๏ εςʔλεόʔ͸Ϟʔυ͕มߋ͞Εͨ࣌ʹߋ৽͢ΔΑ͏ʹ TypeColorSet.theme.subscribe { [weak self] _ in self?.setNeedsStatusBarAppearanceUpdate() }.append(to:

    observationBag) override var preferredStatusBarStyle: UIStatusBarStyle { switch UIColor.currentColorSet().identifier { case .default: return .default case .dark: return .lightContent } } !43
  44. ·ͱΊ w ࢥͬͨΑΓରԠ͕େม w ΋ͱ΋ͱഎܠ৭͕നͷΞϓϦͩͱͲ͜ʹഎܠ৭ࢦఆ͔͕ͨ͠
 ෼͔ΓͮΒ͍ ‎എܠ৭Λҙࣝͯ͠ઃఆ͢Δͷ͕େࣄ w ॎ΍ԣɺ4QMJU7JFXͳͲ༷ʑͳը໘ͷ֬ೝඞཁ ‎શը໘໢ཏͷ6*ςετͱεΫϦʔϯγϣοτ͕ٻΊΒΕΔ

    !44
  45. 13 45

  46. • αʔόʔαΠυΤϯδχΞ (PHP, Golang, AWS) • AndroidΤϯδχΞ (Kotlin) • iOSΤϯδχΞ

    (Swift) TimersͰ͸ݱࡏΤϯδχΞશ৬छ࠾༻தʂ
 ৄ͘͠͸”Timers”Ͱݕࡧ ! 46
  47. ϦδΣΫτίϯ΍Γ·͢ !47 %BZ %BZ

  48. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠. 48