Adaptive Layout は、アプリケーションが異なるデバイスタイプや画面の向きに応じて異なるレイアウトを使用するインターフェースレイアウトのアプローチです。responsive designとは異なり、adaptive layoutはブレークポイントで事前に準備されたレイアウトを切り替えます。この記事では、iOSのSize ClassesとUITraitCollection、Androidのsw600dp/layout-w600dp修飾子について解説し、実践的な例を紹介します。
重要なポイント
Adaptive Layout は、開発者がデバイスタイプごとに個別のレイアウトを作成し、画面の特性に基づいて切り替えるUI戦略です。要素がスムーズに流れるresponsive designとは異なり、adaptive layoutは離散的な切り替えを使用します。電話 — 一つのレイアウト、タブレット — 別のレイアウト、デスクトップ — 三つ目のレイアウトです。
AppleはiOS 8(2014年)でユニバーサルアプリ(iPhoneとiPadの両方で動作するバイナリ)とともにSize Classesを導入しました。GoogleはAndroid 3.2(2011年)でsmallestWidth修飾子(sw
現代のApple HIGとGoogle Material Designのガイドラインは、複数の画面サイズをサポートするアプリにはadaptive layoutが必須であることで一致しています。Google Playはタブレット適応の欠如を欠点としてマークします。App Storeに正式な要件はありませんが、iPadユーザーは引き伸ばされたiPhoneアプリではなく、ネイティブなエクスペリエンスを期待しています。
Size Classes は、各軸で利用可能なスペースをCompact(コンパクト)またはRegular(レギュラー)として定義するiOSの特性です。UITraitCollection は、ビュー階層を通じて環境特性を渡すためのiOSシステムです。サイズ、画面スケール、force touch機能、色域(ライト/ダーク)、アクセシビリティ設定などです。
| デバイス / 向き | 水平 | 垂直 |
|---|---|---|
| iPhone縦向き(全モデル) | Compact | Regular |
| iPhone Plus/Pro Max横向き | Regular | Compact |
| iPhone SE / mini横向き | Compact | Compact |
| iPad縦向き | Regular | Regular |
| iPad横向き | Regular | Regular |
| iPad Split View(画面1/3) | Compact | Regular |
UITraitCollectionはシステムによって形成され、UIApplicationからUIWindowを通じて各UIViewに渡されます。向きやウィンドウサイズ(Split View)が変わると、システムは新しいUITraitCollectionを生成し、traitCollectionDidChangeを呼び出します。SwiftUIでは、変更はEnvironment Valuesを通じて追跡され、ビューは自動的に再描画されます。
UIKitでのUITraitCollectionによる適応は、trait collectionの変更を購読し、レイアウトを再構築することで実装されます。以下の例は、iPhone用の垂直スタックとiPad用の分割レイアウトを切り替えるコントローラを示しています。
// 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は2つの主要な幅修飾子を提供します:sw
| 修飾子 | 条件 | デバイス例 |
|---|---|---|
| layout-sw600dp | 最小幅 ≥ 600 dp | iPad(768 dp)、Pixel C(900 dp) |
| layout-w600dp | 現在の幅 ≥ 600 dp | Split ViewのiPad(600 dp)、横向きの電話 |
| layout-land | 横向き | 水平に回転した任意のデバイス |
| layout-sw720dp | 最小幅 ≥ 720 dp | iPad 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/のベースリソースが使用されます。
現代のAndroid開発では、リソースを直接読み取る代わりにJetpack WindowManagerを使用してウィンドウサイズを決定します。これは、画面サイズが動的に変化する折りたたみデバイス(Galaxy Foldが6.2\"から7.6\"に展開する)で特に重要です。
// 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 は、Size Classesを直接使用せずにアダプティブレイアウトのための組み込みツールを提供します。AnyLayout、ViewThatFits、GeometryReaderを使用すると、利用可能なスペースに自動的に適応するインターフェースを構築できます。
// 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 は、アダプティブレイアウトにWindowSizeClass(Material 3 adaptiveライブラリから)とBoxWithConstraintsを使用します。XML修飾子とは異なり、Composeは実行時にウィンドウサイズを決定し、設定変更時にリアクティブにUIを再構成します。
// 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は、すぐに使えるコンポーネントを提供します:ListDetailScaffold、SupportingPaneScaffold、NavigationSuiteScaffold。これらのコンポーネントはWindowWidthSizeClassに自動的に適応し、スタック(COMPACT)とサイドバイサイド(MEDIUM/EXPANDED)を切り替えます。開発者はパターンを選んでコンテンツパネルを渡すだけです。
よくある質問
sw600dp(smallestWidth)— 向きに関係なく画面の最小幅。7\"+のタブレットでは常に作動します。w600dp(available width)— 現在の向きを考慮した利用可能な幅。幅が600 dpを超える可能性がある横向きの電話で作動します。
UITraitCollection は、環境特性を含むiOSオブジェクトです。サイズクラス(水平/垂直)、ディスプレイスケール、force touch機能、ユーザーインターフェースイディオム(iPhone/iPad)、色域、アクセシビリティ設定などです。UIApplicationからビュー階層を通じて渡され、向きやウィンドウサイズが変わると変化します。
Jetpack WindowManager(Android)とUIScreen nativeBounds + traitCollection(iOS)を使用します。折りたたみデバイスは展開時にSize Classを変更します。Activityを再起動せずにウィンドウサイズの変更をリアクティブに購読する、正規レイアウト(list-detail、supporting pane)に基づくアーキテクチャを推奨します。
いいえ。Size Classのバリエーションを持つ単一のstoryboard、またはUIKitによるプログラムレイアウトを使用してください。SwiftUIはstoryboardをまったく必要としません。iPad用の個別のstoryboardは重複を生み、メンテナンスを複雑にします — Size Classesを通じて単一のレイアウトを適応させる方が良いです。
Canonical layouts はGoogleによる既製のアダプティブレイアウトパターンです。list-detail(リスト+詳細)、supporting pane(コンテンツ+アクションパネル)、feed(カード)があります。各パターンは3つの状態(compact/medium/expanded)をサポートし、Material 3 AdaptiveライブラリでListDetailScaffoldとSupportingPaneScaffoldを通じて実装されています。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。