mcpField
A
axiom-swiftui-layout

axiom-swiftui-layout

github.com/charleswiltgenデザイン・UI1.2k220 回インストール今日更新
リポジトリ

axiom-swiftui-layout について

SwiftUI開発者向けのアダプティブレイアウトスキルです。デバイス機種ではなくコンテナの利用可能な空間に応じて画面を構成する方法を説明し、レイアウトツールの選択、iPadのマルチタスク、iOS 26のウィンドウサイズ変更を扱います。

主な機能

  1. 01ViewThatFitsやAnyLayoutなどの選択
  2. 02onGeometryChangeによる寸法取得
  3. 03Dynamic Typeとサイズクラスへの適応
  4. 04iOS 26のリサイズ可能なウィンドウ
  5. 05レイアウトのアンチパターンと修正案

活用シーン

  1. 01iPhoneとiPadで画面を適応
  2. 02Split ViewやStage Managerのレイアウトを修正
  3. 03iOS 26のウィンドウリサイズに対応
  4. 04実際のウィンドウ寸法に応じてレイアウトを切り替え

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-layout

インストール後、Claude Code で/axiom-swiftui-layoutを入力して読み込まれたか確認します。

詳細

SwiftUI の適応型レイアウト

概要

デバイスを前提とするのではなく、利用可能なスペースに応じて変化するレイアウトを構築するための、規律を徹底するスキルです。ツールの選択、サイズクラスの制約、iOS 26 の自由形式ウィンドウ、よくあるアンチパターンについて説明します。

基本原則: Apple が明日新しいデバイスを発売しても、来年 iPadOS に新しいマルチタスクモードが追加されても、レイアウトが正しく動作するようにしてください。デバイスについての思い込みではなく、コンテナーに応じて対応します。

このスキルを使う場面

  • 「iPad と iPhone の両方でこのレイアウトを動作させるにはどうすればよいですか?」
  • 「GeometryReader と ViewThatFits のどちらを使うべきですか?」
  • 「Split View / Stage Manager でレイアウトが崩れます」
  • 「サイズクラスでは必要な情報が得られません」
  • 「デザイナーから、縦向きと横向きで異なるレイアウトにしてほしいと言われました」
  • 「iOS 26 のウィンドウサイズ変更に向けてアプリを準備しています」

判断ツリー

"I need my layout to adapt..."
│
├─ TO AVAILABLE SPACE (container-driven)
│   │
│   ├─ "Pick best-fitting variant"
│   │   → ViewThatFits
│   │
│   ├─ "Animated switch between H↔V"
│   │   → AnyLayout + condition
│   │
│   ├─ "Read size for calculations"
│   │   → onGeometryChange (iOS 16+)
│   │
│   └─ "Custom layout algorithm"
│       → Layout protocol
│
├─ TO PLATFORM TRAITS
│   │
│   ├─ "Compact vs Regular width"
│   │   → horizontalSizeClass (⚠️ iPad limitations)
│   │
│   ├─ "Accessibility text size"
│   │   → dynamicTypeSize.isAccessibilitySize
│   │
│   └─ "Platform differences"
│       → #if os() / Environment
│
└─ TO WINDOW SHAPE (aspect ratio)
    │
    ├─ "Portrait vs Landscape semantics"
    │   → Geometry + custom threshold
    │
    ├─ "Auto show/hide columns"
    │   → NavigationSplitView (automatic in iOS 26)
    │
    └─ "Window lifecycle"
        → @Environment(\.scenePhase)

ツールの選択

すばやく判断する
Do you need a calculated value (width, height)?
├─ YES → onGeometryChange
└─ NO → Do you need animated transitions?
         ├─ YES → AnyLayout + condition
         └─ NO → ViewThatFits
各ツールを使う場面
必要なこと使うもの使わないもの
2~3 種類のレイアウト候補から選ぶViewThatFitsif size > X
アニメーション付きで H↔V を切り替えるAnyLayout条件付きの HStack/VStack
コンテナーのサイズを読み取るonGeometryChangeGeometryReader
アクセシビリティ向けのテキストに対応するdynamicTypeSize固定のブレークポイント
コンパクトな幅を検出するhorizontalSizeClassUIDevice.idiom
iPad で狭いウィンドウを検出するGeometry + thresholdサイズクラスのみ
サイドバーを表示/非表示にするNavigationSplitView手動のカラム制御
カスタムのレイアウトアルゴリズムを実装するLayout protocolネストした GeometryReader

パターン 1:ViewThatFits

使用する場面: レイアウト候補が 2~3 種類あり、SwiftUI に収まる候補を選ばせたい場合です。

ViewThatFits {
    // First choice: horizontal
    HStack {
        Image(systemName: "star")
        Text("Favorite")
        Spacer()
        Button("Add") { }
    }

    // Fallback: vertical
    VStack {
        HStack {
            Image(systemName: "star")
            Text("Favorite")
        }
        Button("Add") { }
    }
}

制限事項: ViewThatFits では、どの候補が選択されたかを取得できません。その状態をほかのビューでも使う必要がある場合は、代わりに AnyLayout を使います。


パターン 2:アニメーション付きの切り替えに AnyLayout を使う

使用する場面: レイアウト間の切り替えをアニメーションさせたい場合、または現在のレイアウト状態を把握する必要がある場合です。

struct AdaptiveStack<Content: View>: View {
    @Environment(\.horizontalSizeClass) var sizeClass

    let content: Content

    var layout: AnyLayout {
        sizeClass == .compact
            ? AnyLayout(VStackLayout(spacing: 12))
            : AnyLayout(HStackLayout(spacing: 20))
    }

    var body: some View {
        layout {
            content
        }
        .animation(.default, value: sizeClass)
    }
}

Dynamic Type の場合:

@Environment(\.dynamicTypeSize) var dynamicTypeSize

var layout: AnyLayout {
    dynamicTypeSize.isAccessibilitySize
        ? AnyLayout(VStackLayout())
        : AnyLayout(HStackLayout())
}

パターン 3:onGeometryChange(Geometry にはこちらを推奨)

使用する場面: 計算に実際の寸法が必要な場合です。GeometryReader よりこちらを推奨します。

struct ResponsiveGrid: View {
    @State private var columnCount = 2

    var body: some View {
        LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columnCount)) {
            ForEach(items) { item in
                ItemView(item: item)
            }
        }
        .onGeometryChange(for: Int.self) { proxy in
            max(1, Int(proxy.size.width / 150))
        } action: { newCount in
            columnCount = newCount
        }
    }
}

アスペクト比を検出する場合(iPad の「向き」):

struct WindowShapeReader: View {
    @State private var isWide = true

    var body: some View {
        content
            .onGeometryChange(for: Bool.self) { proxy in
                proxy.size.width > proxy.size.height * 1.2
            } action: { newValue in
                isWide = newValue
            }
    }
}

パターン 4:GeometryReader(必要な場合)

使用する場面: iOS 15 以前で Geometry が必要な場合、またはレイアウトフェーズ中に Geometry が必要な場合(副作用として使うだけではない場合)です。

// ✅ CORRECT: Constrained GeometryReader
VStack {
    GeometryReader { geo in
        Text("Width: \(geo.size.width)")
    }
    .frame(height: 44)  // MUST constrain!

    Button("Next") { }
}

// ❌ WRONG: Unconstrained (greedy)
VStack {
    GeometryReader { geo in
        Text("Width: \(geo.size.width)")
    }
    // Takes all available space, crushes siblings
    Button("Next") { }
}

サイズクラスの真理値表(iPad)

構成横方向縦方向
フルスクリーンの縦向き.regular.regular
フルスクリーンの横向き.regular.regular
70% Split View.regular.regular
50% Split View.regular.regular
33% Split View.compact.regular
Slide Over.compact.regular
キーボード使用時(変更なし)(変更なし)

重要なポイント: iPad でサイズクラスが .compact になるのは、幅が約 33% の場合、または Slide Over の場合だけです。より細かく制御するには Geometry を使います。


iOS 26 の自由形式ウィンドウ

変更点
iOS 26 より前iOS 26 以降
固定サイズの Split Viewドラッグによる自由なサイズ変更
UIRequiresFullScreen が使用可能非推奨
iPad にメニューバーなし.commands によるメニューバー
カラムの表示/非表示を手動で制御NavigationSplitView が自動で適応
Apple のガイドライン

「アプリのサイズを変更しても、レイアウトが恒久的に変わらないようにします。可能な限り、開始時の状態に戻れるようにしてください。」

つまり: ウィンドウサイズに基づいてレイアウトの状態を保存しないでください。ウィンドウが元のサイズに戻ったときは、レイアウトも元に戻るようにします。

NavigationSplitView による自動適応
// iOS 26: Columns automatically show/hide
NavigationSplitView {
    Sidebar()
} content: {
    ContentList()
} detail: {
    DetailView()
}
// No manual columnVisibility management needed
移行チェックリスト
  • Info.plist から UIRequiresFullScreen を削除する
  • 任意のウィンドウサイズでテストする(33/50/66% だけでなく)
  • サイズ変更後にレイアウトが「固定」されないことを確認する
  • よく使う操作のためのメニューバーコマンドを追加する
  • Window Controls がツールバー項目に重ならないことをテストする

アンチパターン

❌ デバイスの向きの監視
// ❌ WRONG: Reports device, not window
NotificationCenter.default.addObserver(
    forName: UIDevice.orientationDidChangeNotification, ...
)

let orientation = UIDevice.current.orientation
if orientation.isLandscape { ... }

失敗する理由: ウィンドウの形状ではなく、デバイス本体の向きを報告します。Split View、Stage Manager、iOS 26 では正しく動作しません。

修正方法: onGeometryChange を使って、実際のウィンドウの寸法を読み取ります。

❌ 画面の境界
// ❌ WRONG: Returns full screen, not your window
let width = UIScreen.main.bounds.width
if width > 700 { useWideLayout() }

失敗する理由: マルチタスクでは、アプリが画面の 40% しか使えないことがあります。

修正方法: ビューの実際のコンテナーサイズを読み取ります。

❌ デバイスモデルの判定
// ❌ WRONG: Breaks on new devices, wrong in multitasking
if UIDevice.current.userInterfaceIdiom == .pad {
    useWideLayout()
}

失敗する理由: Split View の 1/3 の iPad は、横向きの iPhone 14 Pro Max より幅が狭くなります。

修正方法: デバイスの種類ではなく、利用可能なスペースに応じて対応します。

❌ 制約のない GeometryReader
// ❌ WRONG: GeometryReader is greedy
VStack {
    GeometryReader { geo in
        Text("Size: \(geo.size)")
    }
    Button("Next") { }  // Crushed
}

修正方法: .frame() で制約するか、onGeometryChange を使います。

❌ 向きの代替指標としてのサイズクラス
// ❌ WRONG: iPad is .regular in both orientations
var isLandscape: Bool {
    horizontalSizeClass == .regular  // Always true on iPad!
}

修正方法: アスペクト比が必要な場合は、実際のジオメトリから計算します。


注意が必要なシナリオ

「デザイナーが iPhone 専用のレイアウトを求めている」

誘惑: if UIDevice.current.userInterfaceIdiom == .phone

回答: 「利用可能なスペースに応じて切り替わる『コンパクト』と『レギュラー』のレイアウトとして実装します。ウィンドウが狭い場合は、iPad にも iPhone のレイアウトが表示されます。これなら Stage Manager と iOS 26 にも対応できます。」

「GeometryReader を使えばいいだけです」

誘惑: すべてを GeometryReader で囲む。

回答: 「GeometryReader には、貪欲に拡張するという既知のレイアウト上の副作用があります。onGeometryChange なら、レイアウトに影響を与えずに同じデータを読み取れます。iOS 16 にバックポートされています。」

「以前はサイズクラスで問題ありませんでした」

誘惑: すべてをサイズクラスで処理する。

回答: 「サイズクラスは大まかです。iPad はどちらの向きでも .regular です。大まかな分類にはサイズクラスを、細かいしきい値にはジオメトリを使います。」

「iPad のマルチタスクには対応しません」

誘惑: UIRequiresFullScreen = true

回答: 「Apple は iOS 26 で、フルスクリーン専用の指定を非推奨にしました。Split View に積極的に対応しない場合でも、サイズ変更でアプリが壊れないようにする必要があります。スペースに基づくレイアウトでも、コストは変わりません。」


リソース

WWDC:2025-208、2024-10074、2022-10056

スキル:axiom-swiftui-layout-ref、axiom-swiftui-debugging、axiom-liquid-glass

コメント