📱

「残念な」iPadアプリをわずかな修正で折り合いをつけるテクニック2選

2023/12/07に公開

iPhoneアプリはビルドターゲットにiPadを追加するだけでiPadアプリとして動かすことが出来ます。しかしiPad向けに一定の最適化をしないとユーザーにとって非常に使い辛いものになってしまいます。残念ながらそうしたiPadアプリがApp Storeには珍しくありません。

今回はそうした「残念な」iPadアプリをわずかな修正で折り合いをつけるための最低限のテクニックを2つ紹介します。

残念な例

前提

  • iPhoneメインのアプリ
  • iPad対応にリソースを割きたくない
  • 一般的なUIパターン(ボトムタブやスタックナビゲーション、リストなど)を採用
struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack {
                List {
                    /* 省略 */
                }
                .navigationTitle("メイン")
                .toolbar {
                    EditButton()
                    ShareLink(item: .init())
                }
            }
            .tabItem { Label("メイン", systemImage: "1.circle") }
            /* 省略 */
        }
    }
}

その1: 横向きに対応する

多くのiPhoneアプリは縦向きのみ対応しています。その流れでiPadアプリも縦向きのみに対応した製品が多くあります。
基本的にiPadユーザーはiPadを横向きで利用します。ユーザーは縦向きにしか対応していないiPadアプリに遭遇した時に大きな苦痛を感じます。
特別な理由がない限り横向きに対応しましょう。

横向き

その2: コンテンツを横に間延びさせない

iPhoneアプリをiPadアプリとして横向きに対応した時に起きる典型的な課題は「コンテンツが横に間延びしすぎて不自然になる事」です。
理想としてはアプリの構成を見直してiPad向けのUIを再定義する事ですが、それでは非常に多くのリソースを消費します。
今回は多くのアプリで採用されている「コンテンツの左右に余白を追加するアプローチ」を紹介します。

左右余白

struct 余白追加: ViewModifier {
    func body(content: Content) -> some View {
        GeometryReader {
            content
                .safeAreaPadding(.horizontal,
                                 $0.size.width > 1100 ? 200 : 0)
        }
    }
}
//===================================
struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack {
                List { 
                    /* 省略 */
                }
                .modifier(余白追加())
                /* 省略 */
            }
            /* 省略 */
        }
    }
}

ListやScrollViewの場合、paddingを左右に追加するだけだと見た目やジェスチャ判定範囲などが崩れてしまいます。今回はsafeAreaの左右を調整する事で余白を追加しました。
こうしたアプローチであればiPhoneアプリの構成を変更する事なくiPadアプリとして使い辛くないデザインにすることが出来ます。

Split Viewは意外と大変

横向きiPadアプリの典型的なデザインとしてサイドバーを用いたSplit Viewがあります。
「ボトムタブをサイドバーに入れ替えただけのselectionベースのSplit View構成」ならリソースを割かずに良い感じに出来そうなので検討してみましょう。

Split View

struct ContentView: View {
    @State private var 選択: String? = "メイン"
    var body: some View {
        NavigationSplitView {
            List(selection: self.$選択) {
                Label("メイン", systemImage: "1.circle").tag("メイン")
                Label("サブ", systemImage: "2.circle").tag("サブ")
                Label("設定", systemImage: "gearshape").tag("設定")
                Label("ヘルプ", systemImage: "info").tag("ヘルプ")
            }
        } detail: {
            switch self.選択 {
                case "メイン": /* 省略 */
                case "サブ": /* 省略 */
                case "設定": /* 省略 */
                case "ヘルプ": /* 省略 */
                default: /* 省略 */
            }
        }
    }
}

実際にこれに挑戦してみると様々な点でリソースを割くことになります。例えば

  • ウインドウの横幅が狭い場合などの様々なシナリオの検討
  • タブ切り替え時のコンテンツViewインスタンスの挙動差のテスト(ボトムタブなら消えず、Split Viewなら消える)
  • detailパネル内のNavigationStackの動作テスト(toolbarやnavigationLinkが想定外の挙動起こしがち)
  • うっかり欲を出して「サイドバー項目の追加」や「ボトムタブとサイドバーの自動切り替え」などの余計な実装に着手

などがあります。

もしSplit Viewの実装経験が無いのであれば、安易にSplit Viewに手を出すことはオススメしません。

補足1: Multiple WindowsをNO

iPhoneと異なりiPadでは複数のウインドウを持つことが出来ます。Xcodeのテンプレート構成やデフォルト設定では複数ウインドウが可能になっています。
想定外のユーザーからの入力による不具合を回避するために複数ウインドウを無効にしましょう。Info.plistの「Enable Multiple Windows」をNOにするだけです。

Info.plist

補足2: iPadでのみ実行するコードの実装方法

UIDevice.current.userInterfaceIdiomを使いましょう。

struct ContentView: View {
    var body: some View {
        switch UIDevice.current.userInterfaceIdiom {
            case .phone: Text("私はiPhone")
            case .pad: Text("私はiPad")
            default: EmptyView()
        }
    }
}

Discussion