SwiftでUISplitViewControllerの表示モードを切り替える

UISplitViewController には複数の表示モードが用意されており、マスター(プライマリ)とディテール(セカンダリ)の見え方を状況に応じて切り替えられます。iPad の広い画面では両カラムを並べて表示し、特定の操作時にはディテールだけを全画面にするといった制御が、preferredDisplayMode プロパティひとつで実現できます。

表示モードの種類

UISplitViewController が提供する主な表示モードは以下のとおりです。

モード説明
automaticシステムが画面サイズに応じて自動選択
oneBesideSecondaryプライマリとセカンダリを横に並べる
oneOverSecondaryプライマリをセカンダリの上に重ねる
secondaryOnlyセカンダリのみを全画面表示

automatic はデフォルト値で、端末の向きやサイズクラスに応じてシステムが最適なレイアウトを選びます。明示的に指定したい場合は、残りの 3 つから用途に合ったものを選択します。

preferredDisplayMode と displayMode の違い

表示モードを扱うプロパティは 2 つあり、それぞれ役割が異なります。

preferredDisplayMode

開発者が「こう表示してほしい」と希望を伝えるプロパティ。設定した値がそのまま採用されるとは限らず、画面サイズや制約によってシステムが調整する場合がある。

displayMode

現在実際に適用されている表示モードを示す読み取り専用プロパティ。preferredDisplayMode に設定した希望が、最終的にどう解決されたかを確認できる。

たとえば iPhone のようなコンパクト幅の環境で oneBesideSecondary を指定しても、物理的に並列表示ができないため、実際の displayMode は異なる値になります。preferredDisplayMode はあくまで「優先的にこのモードを使ってほしい」という要望であり、最終決定権はシステム側にあるという点を理解しておくことが重要です。

基本的な設定方法

SceneDelegate でスプリットビューを構築する際に、preferredDisplayMode を指定します。

func scene(_ scene: UIScene,
           willConnectTo session: UISceneSession,
           options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = (scene as? UIWindowScene) else { return }

    let master = MasterViewController(style: .plain)
    let detail = DetailViewController()

    let masterNav = UINavigationController(rootViewController: master)
    let detailNav = UINavigationController(rootViewController: detail)

    let split = UISplitViewController()
    split.viewControllers = [masterNav, detailNav]
    split.preferredDisplayMode = .oneBesideSecondary

    window = UIWindow(windowScene: windowScene)
    window?.rootViewController = split
    window?.makeKeyAndVisible()
}

oneBesideSecondary を指定すると、iPad の横向きではマスターとディテールが常に横並びで表示されます。ユーザーがスワイプでマスターを隠すことがなくなるため、設定画面のように常時一覧を見せたいアプリに向いています。

ボタンで表示モードを切り替える

実行時にユーザー操作で表示モードを動的に変更する例を見てみましょう。ディテール側のナビゲーションバーにボタンを配置し、タップで切り替えます。

class DetailViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground

        let toggleButton = UIBarButtonItem(
            title: "表示切替",
            style: .plain,
            target: self,
            action: #selector(toggleDisplayMode)
        )
        navigationItem.rightBarButtonItem = toggleButton
    }

    @objc private func toggleDisplayMode() {
        guard let split = splitViewController else { return }

        let newMode: UISplitViewController.DisplayMode
        if split.displayMode == .oneBesideSecondary {
            newMode = .secondaryOnly
        } else {
            newMode = .oneBesideSecondary
        }

        UIView.animate(withDuration: 0.3) {
            split.preferredDisplayMode = newMode
        }
    }
}

splitViewController プロパティを通じて親のスプリットビューにアクセスし、現在の displayMode を見て次のモードを決定しています。UIView.animate で囲むことで、カラムの出現・消失がなめらかにアニメーションします。

displayModeButtonItem を活用する

UISplitViewController には、表示モードを切り替える標準ボタンを返す displayModeButtonItem プロパティが用意されています。自前でトグルロジックを書く代わりに、これを使えば OS 標準の挙動を簡単に組み込めます。

class DetailViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground

        navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
        navigationItem.leftItemsSupplementBackButton = true
    }
}

displayModeButtonItem は、現在の表示状態に応じてアイコンが自動的に変わります。leftItemsSupplementBackButton を true にしておくと、コンパクト表示時のバックボタンと共存できるため、iPhone でも正しく動作します。

このボタンは内部で presentsWithGesture プロパティとも連動しており、スワイプジェスチャーによるマスター表示の切り替えも合わせて制御されます。presentsWithGesture を false にすると、ボタンだけが表示モード切り替えの唯一の手段になります。

スワイプによる意図しない画面遷移を防ぎたい場合に有効な設定。

preferredSplitBehavior との組み合わせ

iOS 14 以降では、preferredSplitBehavior プロパティも追加されました。表示モードとスプリットの振る舞いを組み合わせることで、より細かいレイアウト制御が可能になります。

振る舞い説明
automaticシステムが自動判断
tile両カラムを並べて表示(画面幅を分割)
overlayプライマリをセカンダリの上に重ねる
displaceプライマリ表示時にセカンダリを右へ押しやる
let split = UISplitViewController()
split.preferredDisplayMode = .oneBesideSecondary
split.preferredSplitBehavior = .tile

tile を指定すると、マスターが表示されたときにディテール側の幅が縮まり、両方が画面内に収まるようレイアウトされます。一方 overlay では、マスターがディテールの上に浮かぶ形になるため、ディテールの幅は変わりません。

tile(並べる)

両カラムが画面幅を分け合う。メールアプリのように一覧と詳細を同時に操作したい場合に最適。ディテールの表示領域はマスター分だけ狭くなる。

overlay(重ねる)

マスターがディテールの上にオーバーレイされる。地図アプリのようにディテール側の表示面積を最大限確保したい場合に有効。マスターを閉じれば全画面に戻る。

表示モード変更の検知

表示モードが切り替わったタイミングで何か処理を行いたい場合は、UISplitViewControllerDelegate の splitViewController(_:willChangeTo:) を使います。

class SceneDelegate: UIResponder, UIWindowSceneDelegate,
                     UISplitViewControllerDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }

        let split = UISplitViewController()
        split.delegate = self
        // ... 省略 ...
    }

    func splitViewController(
        _ svc: UISplitViewController,
        willChangeTo displayMode: UISplitViewController.DisplayMode
    ) {
        switch displayMode {
        case .secondaryOnly:
            print("ディテールのみ表示に切り替わります")
        case .oneBesideSecondary:
            print("横並び表示に切り替わります")
        default:
            break
        }
    }
}

デバイスの回転や、ユーザーのスワイプ操作によるモード変更もこのデリゲートで検知できます。たとえばディテール全画面時にはツールバーを表示し、横並び時には非表示にするといったレイアウト調整に活用できます。

実装時の注意点

表示モードの切り替えでよく遭遇する問題は、コンパクト幅の環境での挙動です。iPhone や iPad のスライドオーバーでは、スプリットビューが折りたたまれてナビゲーションスタック形式になるため、preferredDisplayMode の設定が見た目に反映されません。

コンパクト幅での動作

コンパクト幅ではスプリットビューが単一カラムに折りたたまれ、ナビゲーションコントローラーのプッシュ・ポップで画面遷移する。preferredDisplayMode は無視される。

レギュラー幅への復帰

端末を回転させてレギュラー幅に戻ると、最後に設定した preferredDisplayMode が改めて適用される。折りたたみ中に設定した値も保持されているため、復帰後に意図しないモードになっていないか確認が必要。

このため、表示モードに依存するロジックを書く際は displayMode の値を直接チェックし、traitCollection のサイズクラスも合わせて確認するのが安全な実装になります。