📲

SwiftUIでiPhoneとiPadに最適化されたアダプティブレイアウトを実装する

2025/03/03に公開

アダプティブ(Adaptive)に関する概念解説

「アダプティブ(Adaptive)」とは、「適応する」という意味を持つ言葉です。この概念はコンピュータサイエンスの様々な分野で応用されていますが、特にWebデザインとモバイルアプリ開発において重要な役割を果たしています。

アダプティブWebデザイン(Adaptive Web Design)

アダプティブWebデザインとは、ユーザーのデバイスに合わせて複数のWebページバージョンを作成し、適切なものを提供する手法です。これはユーザー体験を向上させるための重要なアプローチとなっています。

特徴:

  • サーバー側でユーザーのデバイスを検出し、最適なレイアウトを選択して表示
  • 一般的に6つの標準的な画面幅(320, 480, 760, 960, 1200, 1600ピクセル)に対応するデザインを用意
  • 異なるデバイスに対して異なるページバージョンを提供する「モバイル別」アプローチ
  • HTTP GETリクエスト時にユーザーエージェントに基づいて最適なページを提供

このアダプティブWebデザインの概念は、デスクトップからモバイルへの移行期に特に重要となりました。現在でも多くのウェブサイトがこの手法を採用しており、詳細はWikipediaのアダプティブウェブデザインのページで確認できます。

なぜアダプティブレイアウトが必要か

Webデザインの進化と同様に、モバイルアプリ開発においてもアダプティブレイアウトの重要性が高まっています。特にAppleのプラットフォームでは、多様なデバイスサイズと向きに対応する必要があります。

Appleのヒューマンインターフェイスガイドライン(HIG)では、下記のように書かれています。

Design a layout that adapts gracefully to context changes while remaining recognizably consistent. People expect your experience to work well and remain familiar when they rotate their device, resize a window, add another display, or switch to a different device.

適切なアダプティブレイアウトを実装しないと、以下のような問題が生じます:

  • iPhoneでは情報が窮屈に表示される
  • iPadでは大部分の画面が空きスペースになり、情報密度が低くなる
  • 端末を回転させたときにレイアウトが崩れる
  • デバイスによって異なるユーザー体験を提供できない

幸いなことに、SwiftUIは「サイズクラス」という概念を通じて、デバイスの種類や向きに応じたレイアウト調整を簡単に実装できるようになっています。

サイズクラスとは何か

Appleのヒューマンインターフェイスガイドラインでは、サイズクラスについて以下のように説明しています:

A size class is a value that's either regular or compact, where regular refers to a larger screen or a screen in landscape orientation and compact refers to a smaller screen or a screen in portrait orientation.

サイズクラスは、画面サイズや向きを「compact(コンパクト)」と「regular(レギュラー)」の2種類の値で表現する概念です。水平方向と垂直方向それぞれにサイズクラスが設定されます。

  • 水平サイズクラス:画面の横幅を表す
  • 垂直サイズクラス:画面の縦幅を表す

HIGの仕様によると、デバイスごとのサイズクラスは以下のように定義されています:

デバイス 縦向き 横向き
iPad Pro, iPad Regular width, Regular height Regular width, Regular height
iPhone Pro Max/Plus Compact width, Regular height Regular width, Compact height
iPhone Pro/標準 Compact width, Regular height Compact width, Compact height

一般的に、iPadはどの向きでも水平サイズクラスがregularになります。一方、iPhoneは縦向きでは水平サイズクラスがcompact、横向きではモデルによって異なります(Plus/Maxモデルではregular、それ以外ではcompact)。

これらのサイズクラスの変化に対応することで、あらゆる状況で最適なレイアウトを提供できます。




サイズクラスを使ったレイアウト実装

SwiftUIでは環境値を使って現在のサイズクラスを簡単に取得できます。これを活用して、デバイスタイプや向きに応じたレイアウトを実装してみましょう。

基本実装:環境値を使ったサイズクラス取得

struct AdaptiveLayoutView: View {
    // 水平・垂直方向のサイズクラスを検出
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    @Environment(\.verticalSizeClass) private var verticalSizeClass
    
    var body: some View {
        // ここでサイズクラスに応じたレイアウトを実装
    }
}

このhorizontalSizeClassverticalSizeClassは、デバイスの向きや画面サイズに応じて自動的に適切な値が設定されます。これを使って条件分岐を行い、最適なレイアウトを選択できます。

4つのレイアウトパターンの実装

サイズクラスの組み合わせに基づいて、4つの異なるレイアウトパターンを実装してみましょう。この例では、水平と垂直のサイズクラスの組み合わせをスイッチ文で分岐しています:

var body: some View {
    VStack(spacing: 0) {
        // デバイス情報とサイズクラスの表示
        deviceInfoHeader
        
        // 水平・垂直サイズクラスの組み合わせに基づいてレイアウトを分ける
        switch (horizontalSizeClass, verticalSizeClass) {
        case (.compact, .regular):
            // iPhoneの縦向き(標準的なiPhone)
            VStack {
                Text("iPhone - 縦向き")
                    .font(.headline)
                    .padding()
                
                phoneLayout
            }
            
        case (.compact, .compact):
            // iPhoneの横向き(標準的なiPhone)
            VStack {
                Text("iPhone - 横向き")
                    .font(.headline)
                    .padding()
                
                phoneLayoutLandscape
            }
            
        case (.regular, .compact):
            // iPadの横向きまたはiPhone Maxの横向き
            VStack {
                Text("iPad - 横向き / iPhone Plus/Max - 横向き")
                    .font(.headline)
                    .padding()
                
                padLayoutLandscape
            }
            
        case (.regular, .regular):
            // iPadの縦向き
            VStack {
                Text("iPad - 縦向き")
                    .font(.headline)
                    .padding()
                
                padLayout
            }
            
        default:
            // 想定外の組み合わせ(基本的には発生しない)
            Text("不明なサイズクラスの組み合わせです")
                .font(.headline)
                .padding()
        }
    }
}

この実装により、以下の4つのケースに対応できます:

  1. iPhone縦向き:(.compact, .regular) → 1列レイアウト
  2. iPhone横向き:(.compact, .compact) → 2列レイアウト
  3. iPadまたはiPhone Max横向き:(.regular, .compact) → 3列レイアウト
  4. iPad縦向き:(.regular, .regular) → 2列レイアウト

各レイアウトの実装

次に、それぞれのレイアウトパターンを実装します。以下は、列数を変えたLazyVGridを使った実装例です:

// iPhone縦向きレイアウト - 1列
private var phoneLayout: some View {
    ScrollView {
        VStack(spacing: 16) {
            explanationText
            
            ForEach(1...6, id: \.self) { index in
                contentCard(index: index)
            }
        }
        .padding()
    }
}

// iPhone横向きレイアウト - 2列
private var phoneLayoutLandscape: some View {
    ScrollView {
        VStack(spacing: 16) {
            explanationText
            
            LazyVGrid(columns: [
                GridItem(.flexible()),
                GridItem(.flexible())
            ], spacing: 16) {
                ForEach(1...6, id: \.self) { index in
                    contentCard(index: index)
                }
            }
        }
        .padding()
    }
}

// iPad縦向きレイアウト - 2列
private var padLayout: some View {
    ScrollView {
        VStack(spacing: 16) {
            explanationText
            
            LazyVGrid(columns: [
                GridItem(.flexible()),
                GridItem(.flexible())
            ], spacing: 16) {
                ForEach(1...6, id: \.self) { index in
                    contentCard(index: index)
                }
            }
        }
        .padding()
    }
}

// iPad横向きレイアウト - 3列
private var padLayoutLandscape: some View {
    ScrollView {
        VStack(spacing: 16) {
            explanationText
            
            LazyVGrid(columns: [
                GridItem(.flexible()),
                GridItem(.flexible()),
                GridItem(.flexible())
            ], spacing: 16) {
                ForEach(1...6, id: \.self) { index in
                    contentCard(index: index)
                }
            }
        }
        .padding()
    }
}

コンテンツカードの実装

各列に表示するコンテンツカードもサイズクラスに応じて調整しています:

private func contentCard(index: Int) -> some View {
    VStack(alignment: .leading, spacing: 12) {
        // カード画像部分
        ZStack {
            Rectangle()
                .fill(Color.blue.opacity(0.2))
                .aspectRatio(horizontalSizeClass == .compact ? 2 : 1.5, contentMode: .fit)
            
            Text("\(index)")
                .font(.largeTitle)
                .fontWeight(.bold)
                .foregroundColor(.white)
        }
        
        // テキストコンテンツ
        VStack(alignment: .leading, spacing: 8) {
            Text("項目 \(index)")
                .font(.headline)
            
            Text("サイズクラスに応じたレイアウト例です。端末を回転させると表示が変わります。iPad向けとiPhone向けで異なるレイアウトを提供しています。")
                .font(.subheadline)
                .foregroundColor(.secondary)
                .lineLimit(horizontalSizeClass == .compact ? 2 : nil)
        }
        .padding(.horizontal, 12)
        .padding(.bottom, 12)
    }
    .background(Color.white)
    .cornerRadius(12)
    .shadow(radius: 2)
}

ここでは、画像の縦横比や表示するテキスト行数など、細かい部分もサイズクラスに応じて調整しています。

実装時の注意点

1. サイズクラスの使い分け

一般的に以下のような使い分けが効果的です:

  • 基本的なレイアウト分岐(グリッド列数など)は水平サイズクラスだけで判断
  • より細かい調整が必要な場合は水平と垂直の組み合わせで判断

2. 環境値の変化に注意

サイズクラスは端末の回転やiPadのSplit View表示などで動的に変化します。レイアウトがこの変化に正しく対応できるように設計しましょう。

3. テストとプレビュー

様々なデバイスとサイズでのレイアウトをテストすることが重要です。SwiftUIのプレビュー機能を活用しましょう:

struct AdaptiveLayoutView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            // iPhone 14 Pro プレビュー
            AdaptiveLayoutView()
                .previewDevice(PreviewDevice(rawValue: "iPhone 14 Pro"))
                .previewDisplayName("iPhone 14 Pro")
            
            // iPad Pro 12.9インチ プレビュー
            AdaptiveLayoutView()
                .previewDevice(PreviewDevice(rawValue: "iPad Pro (12.9-inch) (6th generation)"))
                .previewDisplayName("iPad Pro 12.9")
        }
    }
}

まとめ

SwiftUIのサイズクラスを活用することで、iPhoneとiPadの両方で最適なユーザー体験を提供するアダプティブレイアウトを実装できます。水平・垂直両方のサイズクラスを組み合わせることで、より柔軟で細かな調整が可能になります。

実装のポイントをまとめると、下記のような感じになるかなと思います

  1. 環境値を使ってサイズクラスを取得する
  2. 水平・垂直サイズクラスの組み合わせに基づいてレイアウトを分岐する
  3. 各デバイスと向きに最適なレイアウトを用意する
  4. 詳細な要素(フォントサイズ、画像比率など)もサイズクラスに応じて調整する

この記事で紹介したコード例を参考に、あなたのアプリでもデバイスに最適化されたアダプティブレイアウトを実装してみてください。

またこちらのGitHubのリポジトリを参考にしてください!
https://github.com/entaku0818/samplePlayGround/blob/main/samplePlayGround/samplePlayGround/ResponsiveDesign/AdaptiveLayoutView.swift

参考

https://www.slideshare.net/slideshow/adaptive-ui-114253107
https://en.wikipedia.org/wiki/Adaptive_web_design
https://developer.apple.com/design/human-interface-guidelines/layout
https://en.wikipedia.org/wiki/Adaptation_(computer_science)
https://developer.android.com/codelabs/adaptive-material-guidance?hl=ja#0

Discussion