$30 off During Our Annual Pro Sale. View Details »

デバッグメニューのメンテナンスが大変だったので、専用アプリを作りました。 / iOSDC Japan 2020

FromAtom
September 20, 2020

デバッグメニューのメンテナンスが大変だったので、専用アプリを作りました。 / iOSDC Japan 2020

FromAtom

September 20, 2020
Tweet

More Decks by FromAtom

Other Decks in Technology

Transcript

  1. デバッグメニューの
    メンテナンスが大変だったので、
    専用アプリを作りました。
    iOSDC Japan 2020
    2020/09/20 13:20〜 Track B
    pixiv.inc
    @FromAtom

    View Slide

  2. • pixiv Sketch / pixiv Sketch LIVE
    • アプリ分野テックリードしています
    • 好きなSwiftは guard
    @FromAtom

    View Slide

  3. 原稿も出しました。

    View Slide

  4. 原稿も出しました。
    (なぜか2本も)

    View Slide

  5. めちゃ大変だったので読んで♡

    View Slide

  6. アジェンダ
    • デバッグメニューとは
    • 背景
    • 仕組み
    • 実装方法
    • デバッグメニューアプリ作りのコツ
    • CI時のコツ
    • メリットとデメリット

    View Slide

  7. アジェンダ
    • デバッグメニューとは
    • 背景
    • 仕組み
    • 実装方法
    • デバッグメニューアプリ作りのコツ
    • CI時のコツ
    • メリットとデメリット

    View Slide

  8. デバッグメニューとは?
    一般ユーザには提供されず、
    開発者のみが利用できる機能群

    View Slide

  9. デバッグメニューのメリット
    ビルドせずアプリ内部の状態を確認・変更できる

    View Slide

  10. • 接続するAPIを切り替える
    • Feature Flagを切り替える
    • アカウントの状態を切り替える
    • キャッシュやDBを空にする
    デバッグメニューの機能

    View Slide

  11. デバッグメニューの用途
    • デバッグ
    • 社内テスト
    • ベータテスト

    View Slide

  12. デバッグメニュー
    開発・デバッグの効率化

    View Slide

  13. アジェンダ
    • デバッグメニューとは
    • 背景
    • 仕組み
    • 実装方法
    • デバッグメニューアプリ作りのコツ
    • CI時のコツ
    • メリットとデメリット

    View Slide

  14. 弊社の背景
    • 7つのiOSアプリを開発・提供
    • アプリによって下記が異なる
    ‣ メニューの有無
    ‣ アクセス方法
    ‣ UI
    ‣ 実装方法
    独自デバッグメニュー
    なし
    なし
    独自デバッグメニュー
    独自デバッグメニュー
    なし
    なし

    View Slide

  15. 問題点
    • 社内テストの準備が手間
    • 学習コストが高い
    • 新規導入のコストが高い
    • 開発優先度が低くメンテナンスされにくい
    • 散在する #if DEBUG による事故リスク

    View Slide

  16. 解決案 独自デバッグメニュー
    なし
    なし
    独自デバッグメニュー
    独自デバッグメニュー
    なし
    なし

    View Slide

  17. 解決案
    全アプリのデバッグメニューを
    1つのアプリにする

    View Slide

  18. [ DEMO ]

    View Slide

  19. View Slide

  20. アジェンダ
    • デバッグメニューとは
    • 背景
    • 仕組み
    • 実装方法
    • デバッグメニューアプリ作りのコツ
    • CI時のコツ
    • メリットとデメリット

    View Slide

  21. 仕組み
    App Groups + UserDefaults

    View Slide

  22. App Groups
    同一デベロッパーのアプリ間でデータを共有

    View Slide

  23. 仕組み

    View Slide

  24. UserDefaults
    仕組み

    View Slide

  25. UserDefaults
    UserDefaults
    仕組み

    View Slide

  26. UserDefaults
    UserDefaults
    仕組み

    View Slide

  27. UserDefaults
    UserDefaults
    仕組み

    View Slide

  28. App Groups
    UserDefaults
    UserDefaults
    仕組み

    View Slide

  29. UserDefaults
    UserDefaults
    共用 UserDefaults
    App Groups
    仕組み

    View Slide

  30. App Groups
    UserDefaults
    UserDefaults
    共用 UserDefaults
    仕組み

    View Slide

  31. App Groups
    共用 UserDefaults
    仕組み

    View Slide

  32. 共用 UserDefaults
    isDebug: false
    仕組み

    View Slide

  33. 共用 UserDefaults
    isDebug: true
    isDebug: false
    仕組み

    View Slide

  34. 共用 UserDefaults
    isDebug: true
    isDebug: true
    仕組み

    View Slide

  35. 共用 UserDefaults
    isDebug: true
    isDebug: true
    仕組み

    View Slide

  36. 共用 UserDefaults
    デバッグモードやな
    isDebug: true
    isDebug: true
    仕組み

    View Slide

  37. 共用 UserDefaults
    isDebug: true
    api: "sample.com/api"
    newFeature: false
    仕組み

    View Slide

  38. アジェンダ
    • デバッグメニューとは
    • 背景
    • 仕組み
    • 実装方法
    • デバッグメニューアプリ作りのコツ
    • CI時のコツ
    • メリットとデメリット

    View Slide

  39. App Groupsの設定

    View Slide

  40. App Groupsの設定

    View Slide

  41. App Groupsの設定

    View Slide

  42. App Groupsの設定

    View Slide

  43. App Groupsの設定

    View Slide

  44. App Groupsの設定

    View Slide

  45. App Groupsの設定
    group.com.fromatom.sample.iosdc

    View Slide

  46. App Groupsの設定|Xcode

    View Slide

  47. App Groupsの設定|Xcode

    View Slide

  48. App Groupsの設定|Xcode

    View Slide

  49. App Groupsの設定|Xcode

    View Slide

  50. App Groupsの設定|Xcode

    View Slide

  51. App Groupsの設定|Xcode

    View Slide

  52. App Groupsの設定|Code
    let suiteName = "group.com.fromatom.sample.iosdc"
    let userDefaults = UserDefaults(suiteName: suiteName)
    userDefaults?.set(true, forKey: "isDebug")

    View Slide

  53. これで実装可能

    View Slide

  54. アジェンダ
    • デバッグメニューとは
    • 背景
    • 仕組み
    • 実装方法
    • デバッグメニューアプリ作りのコツ
    • CI時のコツ
    • メリットとデメリット

    View Slide

  55. このアプリのつくりかた

    View Slide

  56. View Slide

  57. UI実装したくない!

    View Slide

  58. 宣言的にUIが生成されるように

    View Slide

  59. App
    import Foundation
    final class App {
    let name: String
    let bundleID: String
    var groups: [Group] = []
    init(name: String, bundleID: String) {
    self.name = name
    self.bundleID = bundleID
    }
    func add(group: Group) {
    groups.append(group)
    }
    }

    View Slide

  60. App(
    name: "iOSDC Japan 2020",
    bundleID: "com.example.iosdc"
    )
    App

    View Slide

  61. let apps = [
    App(name: "iOSDC Japan 2020", bundleID: "com.example.iosdc")
    App(name: "pixiv Sketch", bundleID: "com.example.sketch"),
    App(name: "BOOTH", bundleID: "com.example.booth"),
    App(name: "pixiv", bundleID: "com.example.pixiv"),
    App(name: "pixivコミック", bundleID: "com.example.comic")
    ]
    App

    View Slide

  62. View Slide

  63. Selector

    View Slide

  64. BoolSelector(
    key: Keys.changeAPI.rawValue,
    title: "接続先APIを差し替える"
    )
    Selector(Bool)

    View Slide

  65. StringSelector(
    key: Keys.apiEndpoint.rawValue,
    title: "接続先API",
    values: [
    "https://example.com/api",
    "https://example.com/sandbox-api",
    "https://example.com/staging-api",
    ]
    )
    Selector(String)

    View Slide

  66. let apiGroup = Group(title: "API", selectors: [
    BoolSelector(key: Keys.changeAPI.rawValue, title: "接続先APIを差し替える"),
    StringSelector(key: Keys.apiEndpoint.rawValue, title: "接続先API", values: [
    "https://example.com/api",
    "https://example.com/sandbox-api",
    "https://example.com/staging-api",
    ])
    ])
    Group

    View Slide

  67. let apiGroup = Group(title: "API", selectors: [
    BoolSelector(key: Keys.changeAPI.rawValue, title: "接続先APIを差し替える"),
    StringSelector(key: Keys.apiEndpoint.rawValue, title: "接続先API", values: [
    "https://example.com/api",
    "https://example.com/sandbox-api",
    "https://example.com/staging-api",
    ])
    ])
    let debugModeGroup = Group(title: "DEBUG", selectors: [
    BoolSelector(key: Keys.isDebugMode.rawValue, title: "デバッグモード")
    ])
    let featureGroup = Group(title: "FEATURE", selectors: [
    BoolSelector(key: Keys.featureFlagA.rawValue, title: "新機能Aを有効化"),
    BoolSelector(key: Keys.featureFlagB.rawValue, title: "新機能Bを有効化"),
    BoolSelector(key: Keys.featureFlagC.rawValue, title: "新機能Cを有効化"),
    ])
    Group

    View Slide

  68. let app = App(name: "iOSDC Japan 2020", bundleID: "com.example.iosdc")
    let debugModeGroup = Group(title: "DEBUG", selectors: [
    BoolSelector(key: Keys.isDebugMode.rawValue, title: "デバッグモード")
    ])
    app.add(group: debugModeGroup)
    let apiGroup = Group(title: "API", selectors: [
    BoolSelector(key: Keys.changeAPI.rawValue, title: "接続先APIを差し替える"),
    StringSelector(key: Keys.apiEndpoint.rawValue, title: "接続先API", values: [
    "https://example.com/api",
    "https://example.com/sandbox-api",
    "https://example.com/staging-api",
    ])
    ])
    app.add(group: apiGroup)
    let featureGroup = Group(title: "FEATURE", selectors: [
    BoolSelector(key: Keys.featureFlagA.rawValue, title: "新機能Aを有効化"),
    BoolSelector(key: Keys.featureFlagB.rawValue, title: "新機能Bを有効化"),
    BoolSelector(key: Keys.featureFlagC.rawValue, title: "新機能Cを有効化"),
    ])
    app.add(group: featureGroup)

    View Slide

  69. let app = App(name: "iOSDC Japan 2020", bundleID: "com.example.iosdc")
    let debugModeGroup = Group(title: "DEBUG", selectors: [
    BoolSelector(key: Keys.isDebugMode.rawValue, title: "デバッグモード")
    ])
    app.add(group: debugModeGroup)
    let apiGroup = Group(title: "API", selectors: [
    BoolSelector(key: Keys.changeAPI.rawValue, title: "接続先APIを差し替える"),
    StringSelector(key: Keys.apiEndpoint.rawValue, title: "接続先API", values: [
    "https://example.com/api",
    "https://example.com/sandbox-api",
    "https://example.com/staging-api",
    ])
    ])
    app.add(group: apiGroup)
    let featureGroup = Group(title: "FEATURE", selectors: [
    BoolSelector(key: Keys.featureFlagA.rawValue, title: "新機能Aを有効化"),
    BoolSelector(key: Keys.featureFlagB.rawValue, title: "新機能Bを有効化"),
    BoolSelector(key: Keys.featureFlagC.rawValue, title: "新機能Cを有効化"),
    ])
    app.add(group: featureGroup)

    View Slide

  70. let app = App(name: "iOSDC Japan 2020", bundleID: "com.example.iosdc")
    let debugModeGroup = Group(title: "DEBUG", selectors: [
    BoolSelector(key: Keys.isDebugMode.rawValue, title: "デバッグモード")
    ])
    app.add(group: debugModeGroup)
    let apiGroup = Group(title: "API", selectors: [
    BoolSelector(key: Keys.changeAPI.rawValue, title: "接続先APIを差し替える"),
    StringSelector(key: Keys.apiEndpoint.rawValue, title: "接続先API", values: [
    "https://example.com/api",
    "https://example.com/sandbox-api",
    "https://example.com/staging-api",
    ])
    ])
    app.add(group: apiGroup)
    let featureGroup = Group(title: "FEATURE", selectors: [
    BoolSelector(key: Keys.featureFlagA.rawValue, title: "新機能Aを有効化"),
    BoolSelector(key: Keys.featureFlagB.rawValue, title: "新機能Bを有効化"),
    BoolSelector(key: Keys.featureFlagC.rawValue, title: "新機能Cを有効化"),
    ])
    app.add(group: featureGroup)

    View Slide

  71. let app = App(name: "iOSDC Japan 2020", bundleID: "com.example.iosdc")
    let debugModeGroup = Group(title: "DEBUG", selectors: [
    BoolSelector(key: Keys.isDebugMode.rawValue, title: "デバッグモード")
    ])
    app.add(group: debugModeGroup)
    let apiGroup = Group(title: "API", selectors: [
    BoolSelector(key: Keys.changeAPI.rawValue, title: "接続先APIを差し替える"),
    StringSelector(key: Keys.apiEndpoint.rawValue, title: "接続先API", values: [
    "https://example.com/api",
    "https://example.com/sandbox-api",
    "https://example.com/staging-api",
    ])
    ])
    app.add(group: apiGroup)
    let featureGroup = Group(title: "FEATURE", selectors: [
    BoolSelector(key: Keys.featureFlagA.rawValue, title: "新機能Aを有効化"),
    BoolSelector(key: Keys.featureFlagB.rawValue, title: "新機能Bを有効化"),
    BoolSelector(key: Keys.featureFlagC.rawValue, title: "新機能Cを有効化"),
    ])
    app.add(group: featureGroup)

    View Slide

  72. 参考実装
    https://github.com/FromAtom/iosdc-2020-sample

    View Slide

  73. YAMLやTOMLを使ったら?

    View Slide

  74. YAMLやTOMLを使わない理由
    • 正しいデータ形式か確認する機構の実装が必須
    ‣ YAML, TOMLとしての正しさ
    ‣ Chushaが扱う形として正しさ
    • Swiftで書けばコンパイラが正しいか見てくれる
    • [個人の感想] 読み書きするのキツくない?

    View Slide

  75. アジェンダ
    • デバッグメニューとは
    • 背景
    • 仕組み
    • 実装方法
    • デバッグメニューアプリ作りのコツ
    • CI時のコツ
    • メリットとデメリット

    View Slide

  76. CIで生成するもの
    • 実機用の ".ipa"
    • シミュレーター用の ".app"

    View Slide

  77. CIで生成するもの
    • 実機用の ".ipa"
    • シミュレーター用の ".app"

    View Slide

  78. ".app" の作り方
    ビルド方法
    lane :build_app_file do
    xcodebuild(
    scheme: 'Chusha',
    build: true,
    sdk: 'iphonesimulator',
    derivedDataPath: 'build'
    )
    end
    Fastfile Bitrise
    OR

    View Slide

  79. ".app" の作り方
    GitHub Releasesにuploadする
    set -eu
    go get -u github.com/tcnksm/ghr
    version=0.0.1
    name=chusha-${version}.tar.gz
    mkdir -p releases
    cp -r build/Build/Products/Debug-iphonesimulator/Chusha.app releases/Chucha.app
    tar zcvf $name releases
    body="ADHOC: ${ADHOC_INSTALL_PAGE_URL}
    INHOUSE: ${INHOUSE_INSTALL_PAGE_URL}"
    echo $ADHOC_INSTALL_PAGE_URL
    echo $body
    ghr -delete -n $version -b "${body}" $version $name

    View Slide

  80. ".app" の作り方
    GitHub Releasesにuploadする
    set -eu
    go get -u github.com/tcnksm/ghr
    version=0.0.1
    name=chusha-${version}.tar.gz
    mkdir -p releases
    cp -r build/Build/Products/Debug-iphonesimulator/Chusha.app releases/Chucha.app
    tar zcvf $name releases
    body="ADHOC: ${ADHOC_INSTALL_PAGE_URL}
    INHOUSE: ${INHOUSE_INSTALL_PAGE_URL}"
    echo $ADHOC_INSTALL_PAGE_URL
    echo $body
    ghr -delete -n $version -b "${body}" $version $name

    View Slide

  81. ".app" の作り方
    GitHub Releasesにuploadする
    set -eu
    go get -u github.com/tcnksm/ghr
    version=0.0.1
    name=chusha-${version}.tar.gz
    mkdir -p releases
    cp -r build/Build/Products/Debug-iphonesimulator/Chusha.app releases/Chucha.app
    tar zcvf $name releases
    body="ADHOC: ${ADHOC_INSTALL_PAGE_URL}
    INHOUSE: ${INHOUSE_INSTALL_PAGE_URL}"
    echo $ADHOC_INSTALL_PAGE_URL
    echo $body
    ghr -delete -n $version -b "${body}" $version $name

    View Slide

  82. ".app" の作り方

    View Slide

  83. ".app" の作り方

    View Slide

  84. ".app" の作り方

    View Slide

  85. View Slide

  86. アジェンダ
    • デバッグメニューとは
    • 背景
    • 仕組み
    • 実装方法
    • デバッグメニューアプリ作りのコツ
    • CI時のコツ
    • メリットとデメリット

    View Slide

  87. メリット
    • 1つのアプリで複数のアプリにデバッグ情報を注入できる
    • 起動した時点でデバッグ情報が注入されている
    ‣ 初回起動時になにかしたい時に便利
    • App Storeに公開されているアプリでも使える
    • #if DEBUG の利用が減らせる
    ※登壇後追記:収録後の2020年9月11日にレビューガイドラインが更新され、下記の通りガイドラインの明確化されました。
    2.3.1: Don’t include any hidden, dormant, or undocumented features in your app; your app’s functionality should be clear to end users and App Review.
    このため、App Storeに公開されるバージョンでもChushaが利用できると、リジェクトされる可能性があります。あらかじめご了承ください。

    View Slide

  88. デメリット
    • Chushaのインストールが必要になる
    • 別アプリに移動しないといけない
    • 即時反映系の機能が作りにくい
    ‣ DBクリア、キャッシュクリア

    View Slide

  89. まとめ
    • AppGroupsとUserDefaultsでデバッグメニューをアプリ化
    • デバッグメニューアプリは宣言的にUIを実装
    • ".app" を生成しておけばシミュレータでも使える

    View Slide