Adaptive Layout: 重要な概念、サイズクラスと修飾子

著者: IT Sectr 公開日: 2026-02-26 読了時間: 10 分

Adaptive Layout は、アプリケーションが異なるデバイスタイプや画面の向きに応じて異なるレイアウトを使用するインターフェースレイアウトのアプローチです。responsive designとは異なり、adaptive layoutはブレークポイントで事前に準備されたレイアウトを切り替えます。この記事では、iOSのSize ClassesとUITraitCollection、Androidのsw600dp/layout-w600dp修飾子について解説し、実践的な例を紹介します。

重要なポイント

  • Adaptive Layout — 画面サイズ、向き、デバイスタイプに応じた離散的なレイアウト切り替え
  • Size Classes iOSでは、スペースを水平・垂直方向にCompactまたはRegularとして分類
  • UITraitCollection — iOS環境特性のコンテナ(サイズ、スケール、force touch、色域)
  • sw600dp — タブレット(7\"以上)向けAndroid最小幅修飾子
  • layout-w600dp — 現在の向きで十分なスペースがある場合に作動する利用可能幅修飾子

Adaptive Layoutとは?

Adaptive Layout は、開発者がデバイスタイプごとに個別のレイアウトを作成し、画面の特性に基づいて切り替えるUI戦略です。要素がスムーズに流れるresponsive designとは異なり、adaptive layoutは離散的な切り替えを使用します。電話 — 一つのレイアウト、タブレット — 別のレイアウト、デスクトップ — 三つ目のレイアウトです。

AppleはiOS 8(2014年)でユニバーサルアプリ(iPhoneとiPadの両方で動作するバイナリ)とともにSize Classesを導入しました。GoogleはAndroid 3.2(2011年)でsmallestWidth修飾子(swdp)を発表しました。両方のメカニズムは同じ問題を解決します:開発者にコードの重複なしで異なる画面向けに異なるUIを作成するツールを提供することです。

現代のApple HIGとGoogle Material Designのガイドラインは、複数の画面サイズをサポートするアプリにはadaptive layoutが必須であることで一致しています。Google Playはタブレット適応の欠如を欠点としてマークします。App Storeに正式な要件はありませんが、iPadユーザーは引き伸ばされたiPhoneアプリではなく、ネイティブなエクスペリエンスを期待しています。

iOSのSize ClassesとUITraitCollection

Size Classes は、各軸で利用可能なスペースをCompact(コンパクト)またはRegular(レギュラー)として定義するiOSの特性です。UITraitCollection は、ビュー階層を通じて環境特性を渡すためのiOSシステムです。サイズ、画面スケール、force touch機能、色域(ライト/ダーク)、アクセシビリティ設定などです。

デバイス / 向き水平垂直
iPhone縦向き(全モデル)CompactRegular
iPhone Plus/Pro Max横向きRegularCompact
iPhone SE / mini横向きCompactCompact
iPad縦向きRegularRegular
iPad横向きRegularRegular
iPad Split View(画面1/3)CompactRegular

UITraitCollectionはシステムによって形成され、UIApplicationからUIWindowを通じて各UIViewに渡されます。向きやウィンドウサイズ(Split View)が変わると、システムは新しいUITraitCollectionを生成し、traitCollectionDidChangeを呼び出します。SwiftUIでは、変更はEnvironment Valuesを通じて追跡され、ビューは自動的に再描画されます。

Swiftによるアダプティブレイアウトの例

UIKitでのUITraitCollectionによる適応は、trait collectionの変更を購読し、レイアウトを再構築することで実装されます。以下の例は、iPhone用の垂直スタックとiPad用の分割レイアウトを切り替えるコントローラを示しています。

swift
// Size Classesを使用したアダプティブコントローラ
import UIKit

final class AdaptiveViewController: UIViewController {

    private let compactStack = UIStackView()
    private let regularStack = UIStackView()
    private let sidebar = UIView()
    private let content = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        updateLayout(for: traitCollection)
    }

    override func traitCollectionDidChange(
        _ previousTraitCollection: UITraitCollection?
    ) {
        super.traitCollectionDidChange(previousTraitCollection)
        if traitCollection.horizontalSizeClass
            != previousTraitCollection?.horizontalSizeClass {
            updateLayout(for: traitCollection)
        }
    }

    private func updateLayout(for traits: UITraitCollection) {
        if traits.horizontalSizeClass == .regular {
            showRegularLayout() // iPad: サイドバー + コンテンツ
        } else {
            showCompactLayout() // iPhone: タブバー + プッシュ
        }
    }

    private func showRegularLayout() {
        view.subviews.forEach { $0.removeFromSuperview() }
        regularStack.addArrangedSubview(sidebar)
        regularStack.addArrangedSubview(content)
        regularStack.frame = view.bounds
        regularStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(regularStack)
    }

    private func showCompactLayout() {
        view.subviews.forEach { $0.removeFromSuperview() }
        compactStack.addArrangedSubview(content)
        compactStack.frame = view.bounds
        compactStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(compactStack)
    }

    private func setupViews() {
        sidebar.backgroundColor = .systemGray6
        content.backgroundColor = .systemBackground
        compactStack.axis = .vertical
        regularStack.axis = .horizontal
    }
}

traitCollectionDidChangeメソッドは任意のトレイト変更で呼び出されますが、レイアウト切り替えの最も重要な指標であるhorizontalSizeClassのみをチェックします。iOS 17+では、変更を購読せずにリアクティブな更新を行うために、UIViewController.horizontalSizeClassを計算プロパティとして使用することを推奨します。

Android修飾子: sw600dpとw600dp

Androidは2つの主要な幅修飾子を提供します:swdp(smallestWidth)とwdp(available width)。違いは重要です。swは向きに関係なく画面の最小幅、wは現在の向きを考慮した利用可能な幅です。swは、指定された最小幅を物理的に持つデバイスに対してレイアウトが読み込まれることを保証します。wは回転に応答します — 横向きでは、縦向きの幅が360 dpでもw600dpが作動する可能性があります。

修飾子条件デバイス例
layout-sw600dp最小幅 ≥ 600 dpiPad(768 dp)、Pixel C(900 dp)
layout-w600dp現在の幅 ≥ 600 dpSplit ViewのiPad(600 dp)、横向きの電話
layout-land横向き水平に回転した任意のデバイス
layout-sw720dp最小幅 ≥ 720 dpiPad Pro(833 dp)、Galaxy Tab S9(800 dp)

Androidのリソース階層:res/layout/activity_main.xml(電話)、res/layout-sw600dp/activity_main.xml(7\"タブレット)、res/layout-w600dp-land/activity_main.xml(横向きの電話)。Androidは現在のデバイス設定に最も適した修飾子を選択します。一致するファイルがない場合は、res/layout/のベースリソースが使用されます。

Kotlinによるアダプティブレイアウトの例

現代のAndroid開発では、リソースを直接読み取る代わりにJetpack WindowManagerを使用してウィンドウサイズを決定します。これは、画面サイズが動的に変化する折りたたみデバイス(Galaxy Foldが6.2\"から7.6\"に展開する)で特に重要です。

kotlin
// Jetpack WindowManagerによる適応
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AdaptiveScreen() {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass

    FlowRow(
        verticalAlignment = Arrangement.Top,
        horizontalArrangement = when (windowSizeClass.windowWidthSizeClass) {
            WindowWidthSizeClass.EXPANDED -> Arrangement.SpaceEvenly
            else -> Arrangement.Start
        }
    ) {
        when (windowSizeClass.windowWidthSizeClass) {
            WindowWidthSizeClass.COMPACT -> {
                CompactContent()
            }
            WindowWidthSizeClass.MEDIUM -> {
                MediumContent()
            }
            WindowWidthSizeClass.EXPANDED -> {
                ExpandedContent()
            }
        }
    }
}

@Composable
private fun CompactContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("電話", style = MaterialTheme.typography.headlineSmall)
        ListContent()
    }
}

@Composable
private fun MediumContent() {
    Row(modifier = Modifier.padding(24.dp)) {
        NavigationRail { ... }
        Column { ListContent() }
    }
}

@Composable
private fun ExpandedContent() {
    Row(modifier = Modifier.padding(32.dp)) {
        PermanentNavigationDrawer { ... }
        Column {
            ListContent()
        }
        DetailPane()
    }
}

androidx.windowライブラリのcurrentWindowAdaptiveInfo()コンポーネントは、電話、タブレット、折りたたみ、ChromeOSなど任意のデバイスにインターフェースを適応させます。幅クラス:COMPACT(0–600 dp)、MEDIUM(600–840 dp)、EXPANDED(>840 dp)。高さクラス:COMPACT(0–480 dp)、MEDIUM(480–900 dp)、EXPANDED(>900 dp)。

SwiftUIのAdaptive Layout

SwiftUI は、Size Classesを直接使用せずにアダプティブレイアウトのための組み込みツールを提供します。AnyLayout、ViewThatFits、GeometryReaderを使用すると、利用可能なスペースに自動的に適応するインターフェースを構築できます。

swift
// AnyLayoutとViewThatFitsによるSwiftUI適応
import SwiftUI

struct AdaptiveGrid: View {
    @Environment(\.horizontalSizeClass) private var hSizeClass

    var body: some View {
        let layout = hSizeClass == .regular
            ? AnyLayout(HStackLayout())
            : AnyLayout(VStackLayout())

        layout {
            Label("お気に入り", systemImage: "star")
            Label("最近", systemImage: "clock")
            Label("設定", systemImage: "gear")
        }
        .padding()
    }
}

// ViewThatFits — 自動レイアウト選択
struct SmartLayout: View {
    var body: some View {
        ViewThatFits {
            HStack { // 優先度1: 水平
                CardView()
                CardView()
            }
            VStack { // 収まらない場合 — 垂直
                CardView()
                CardView()
            }
        }
    }
}

ViewThatFits は、クリッピングなしで利用可能なスペースに収まる最初の子ビューを自動的に選択する強力なSwiftUIツールです。これにより、単純なアダプティブ切り替えのためにSize Classesを明示的にチェックする必要がなくなります。AnyLayout を使用すると、ビュー本文でif-elseを使わずにレイアウトタイプ(HStack/VStack)を切り替えられます。

Jetpack ComposeのAdaptive Layout

Jetpack Compose は、アダプティブレイアウトにWindowSizeClass(Material 3 adaptiveライブラリから)とBoxWithConstraintsを使用します。XML修飾子とは異なり、Composeは実行時にウィンドウサイズを決定し、設定変更時にリアクティブにUIを再構成します。

kotlin
// BoxWithConstraintsによるJetpack Composeアダプティブ
@Composable
fun AdaptiveList(items: List<String>) {
    BoxWithConstraints {
        val width = maxWidth

        if (width >= 600.dp) {
            // タブレット: 2カラム(list-detail)
            Row(modifier = Modifier.fillMaxSize()) {
                LazyColumn(modifier = Modifier.weight(1f)) {
                    items(items) { item ->
                        ListItem(text = item)
                    }
                }
                var selected by remember { mutableStateOf(items.first()) }
                DetailPanel(item = selected)
                    .weight(2f)
            }
        } else {
            // 電話: ナビゲーション付き1カラム
            LazyColumn(modifier = Modifier.fillMaxSize()) {
                items(items) { item ->
                    ListItem(text = item, onClick = { navigateToDetail(item) })
                }
            }
        }
    }
}

@Composable
fun AdaptiveListMaterial3() {
    val windowClass = currentWindowAdaptiveInfo().windowSizeClass

    AdaptiveLayout(
        layout = windowClass.windowWidthSizeClass
    ) {
        when (windowClass.windowWidthSizeClass) {
            WindowWidthSizeClass.COMPACT -> {
                ListDetailScaffold(
                    isDetailOnly = false,
                    list = { ListPane() },
                    detail = { DetailPane() }
                )
            }
            else -> {
                ListDetailScaffold(
                    isDetailOnly = false,
                    list = { ListPane() },
                    detail = { DetailPane() }
                )
            }
        }
    }
}

Material 3 Adaptive Layoutsは、すぐに使えるコンポーネントを提供します:ListDetailScaffoldSupportingPaneScaffoldNavigationSuiteScaffold。これらのコンポーネントはWindowWidthSizeClassに自動的に適応し、スタック(COMPACT)とサイドバイサイド(MEDIUM/EXPANDED)を切り替えます。開発者はパターンを選んでコンテンツパネルを渡すだけです。

よくある質問

sw600dpとw600dpの違いは何ですか?

sw600dp(smallestWidth)— 向きに関係なく画面の最小幅。7\"+のタブレットでは常に作動します。w600dp(available width)— 現在の向きを考慮した利用可能な幅。幅が600 dpを超える可能性がある横向きの電話で作動します。

iOSのUITraitCollectionとは何ですか?

UITraitCollection は、環境特性を含むiOSオブジェクトです。サイズクラス(水平/垂直)、ディスプレイスケール、force touch機能、ユーザーインターフェースイディオム(iPhone/iPad)、色域、アクセシビリティ設定などです。UIApplicationからビュー階層を通じて渡され、向きやウィンドウサイズが変わると変化します。

折りたたみデバイス向けにアプリを適応させるには?

Jetpack WindowManager(Android)とUIScreen nativeBounds + traitCollection(iOS)を使用します。折りたたみデバイスは展開時にSize Classを変更します。Activityを再起動せずにウィンドウサイズの変更をリアクティブに購読する、正規レイアウト(list-detail、supporting pane)に基づくアーキテクチャを推奨します。

iPad用に別のstoryboardを作成する必要がありますか?

いいえ。Size Classのバリエーションを持つ単一のstoryboard、またはUIKitによるプログラムレイアウトを使用してください。SwiftUIはstoryboardをまったく必要としません。iPad用の個別のstoryboardは重複を生み、メンテナンスを複雑にします — Size Classesを通じて単一のレイアウトを適応させる方が良いです。

Material 3の正規レイアウト(canonical layouts)とは何ですか?

Canonical layouts はGoogleによる既製のアダプティブレイアウトパターンです。list-detail(リスト+詳細)、supporting pane(コンテンツ+アクションパネル)、feed(カード)があります。各パターンは3つの状態(compact/medium/expanded)をサポートし、Material 3 AdaptiveライブラリでListDetailScaffoldとSupportingPaneScaffoldを通じて実装されています。

まとめ

  • Adaptive Layout — 異なるデバイス向けにブレークポイントで離散的にレイアウトを切り替え
  • Size Classes iOS(Compact/Regular)がiPhone、iPad、Split Viewのレイアウトを定義
  • UITraitCollection — ビュー階層を通じて環境特性を渡すメカニズム
  • sw600dp — 7\"+タブレット向けAndroid最小幅修飾子
  • w600dp — 横向きに回転すると作動する利用可能幅修飾子
  • Jetpack WindowManager がWindowSizeClassを提供(WindowWidthSizeClass: COMPACT/MEDIUM/EXPANDED)
  • Material 3 Adaptive — 自動適応付き既製コンポーネント(ListDetailScaffold)

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください