【Swift】UISplitViewControllerを使ってiPhoneとiPadのUIを共通のコードで実現させる


この記事はニフティグループアドベントカレンダー22日目の記事です。
昨日は@jimmysharpさんで「監視システムPrometheusとTICK Stackを両方使ってみた」でした。

完成形

以下の動きを共通のコードで実現させることを目指します!
※設定アプリのような動きです

iPhone iPad

背景

私が担当しているアプリでは↑に上げた完成形のような動きをします。
ですが、iPhoneとiPadで別々のコードで書かれており苦労したことが多々あったので、共通化のためのサンプルコードを作ろうと思いました。

どこに苦労したのか

  • 画面の生成方法が違う
    • iPhone : UISplitViewController 使わない
    • iPad : UISplitViewController 使う
    • 画面の初期化処理を2つ書かなければならず、変更時に片方書き忘れることがある
  • 詳細への画面遷移の方法が違う
    • iPhone : 普通に pushViewController(_:animated:)
    • iPad : 詳細画面が生成されているかチェックし、あれば使う、なければ生成して使う
    • iPadのときは常にinitを通るわけではないので、メソッドやプロパティへの直アクセスなどで依存するものを注入する必要があった
      • nilかどうか気にしなきゃいけない
      • いつ初期化されるかわからない
  • 上記の違いを吸収するコードを書かなければならない
    • いたるところに if iPhone {} if iPad {} ...
    • iPhoneでは動くけどiPadのこと考慮から漏れてた!!というのが1度や2度じゃない…
    • そして、吸収できないことが多々…🤮

やってられん!!😡
ということで改善していきます!!💪

本題に入る前に…

私が担当するアプリでは

  • iPhone : portrait(縦)固定
  • iPad : landscape(横)固定

という仕様になっているため、サンプルコードも同様の設定にしてます。
画面回転を対応したい場合は、 UIViewControllertraitCollectionDidChange(_:) でSizeClassの変更を検知して対応してください。

[iOS 8] マルチデバイス対応の新機能「Trait Collection」

共通化の手順

インプット

作ったもの

サンプルコード
GitHubAPIから取得したリポジトリ情報をTableViewに表示し、セルを押下すると該当のリポジトリのWebページを表示します。

肝は以下2つです。それぞれ説明します

  • SplitViewController
  • showDetailViewController(_:sender:)

SplitViewController

SplitViewController.swift
class SplitViewController: UISplitViewController, UISplitViewControllerDelegate {

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()

        // 表示できるならMasterとDetail両方表示させる
        preferredDisplayMode = .allVisible

        delegate = self

        viewControllers = [
            UINavigationController(rootViewController: master),
            detail
        ]
    }

    // 横幅が小さいときDetailをどう表示するかを決めるdelegate
    // true  : Detailを隠す
    // false : Detailを表示させる
    func splitViewController(_ splitViewController: UISplitViewController,
                             collapseSecondary secondaryViewController: UIViewController,
                             onto primaryViewController: UIViewController) -> Bool {
        return splitViewController.isCollapseSecondary
    }
}

extension UISplitViewController {

    /// Detailを隠すかどうか
    var isCollapseSecondary: Bool {
        // 横幅が狭かったらDetailを隠す
        return traitCollection.containsTraits(in: UITraitCollection(horizontalSizeClass: .compact))
    }
}

showDetailViewController(_:sender:)

SearchResultRouter.swift
class SearchResultViewController: UITableViewDelegate, UITableViewDataSource {

    // ...

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let url = repositories[indexPath.row].htmlUrl
        let detailView = DetailViewController(url: url)
        // Detailに表示されるかpushViewControllerされるかはおまかせ
        showDetailViewController(detailView, sender: self)
    }
}

まとめ

  • iOS8からiPhoneでも使えるようになった UISplitViewController を活用して、iPhoneとiPadのUIを同一コードで実現できました。
    • Detailを一緒に表示させるかどうか
    • 画面遷移のロジックを統一
  • このサンプルを活用して、担当アプリも改善していきたいと思います!!💪💪
  • UIの共通化つながりですが、SizeClassを用いてViewの並びや表示するコンポーネントを切り替えることもできますので、合わせてご参照ください。私もあとで試してみたいと思います。
  • 最初は弊社で採用しているVIPERアーキテクチャで実装していましたが、サンプルにしてはあまりに壮大になったのでやめました。masterブランチはVIPERアーキテクチャでの実装になってますので気になる方は御覧ください。

最後に

明日は@taka_masaさんが「今年イチの脆弱性」についてお話いただけるそうです!お楽しみに!