mcpField
A
axiom-swiftui-layout

axiom-swiftui-layout

github.com/charleswiltgen设计与 UI1.2k220 次安装今天更新
代码仓库

关于 axiom-swiftui-layout

面向 SwiftUI 开发者的自适应布局技能,指导界面根据容器可用空间而非设备型号调整。内容涵盖布局工具选择、iPad 多任务场景和 iOS 26 可调整窗口。

主要功能

  1. 01ViewThatFits、AnyLayout 等工具选择
  2. 02使用 onGeometryChange 读取尺寸
  3. 03适配动态字体和尺寸类别
  4. 04iOS 26 自由调整窗口指导
  5. 05布局反模式与修正建议

适用场景

  1. 01让界面适配 iPhone 与 iPad
  2. 02修复 Split View 或 Stage Manager 中的布局
  3. 03支持 iOS 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?”
  • “我的布局在分屏 / 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 上的窄窗口几何信息 + 阈值仅使用尺寸类别
隐藏 / 显示侧边栏NavigationSplitView手动控制列逻辑
自定义布局算法Layout 协议嵌套 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)
    }
}

用于动态字体:

@Environment(\.dynamicTypeSize) var dynamicTypeSize

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

模式 3:onGeometryChange(几何信息首选方案)

**适用场景:**需要获取实际尺寸进行计算。相比 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 或更早版本;或者需要在布局阶段获取几何信息(而不只是作为副作用)。

// ✅ 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% 分屏.regular.regular
50% 分屏.regular.regular
33% 分屏.compact.regular
滑动覆盖.compact.regular
使用键盘时(不变)(不变)

**关键要点:**在 iPad 上,尺寸类别只有在宽度约为 33% 或处于滑动覆盖模式时才会变为 .compact。如需更精细的控制,请使用几何信息。


iOS 26 自由调整窗口

变化内容
iOS 26 之前iOS 26+
固定的分屏尺寸可自由拖动调整大小
允许使用 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%)
  • 确认调整大小后布局不会“卡住”
  • 为常见操作添加菜单栏命令
  • 测试窗口控件不会与工具栏项目重叠

反模式

❌ 设备方向观察器
// ❌ WRONG: Reports device, not window
NotificationCenter.default.addObserver(
    forName: UIDevice.orientationDidChangeNotification, ...
)

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

**为何不可用:**报告的是设备的物理方向,而非窗口形状。在分屏视图、台前调度和 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()
}

**为何不可用:**处于三分之一分屏视图的 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 布局。这样可以确保我们面向台前调度和 iOS 26。”

“直接用 GeometryReader 就行,没问题”

**诱惑:**把所有内容都包在 GeometryReader 中。

回应:“GeometryReader 有已知的布局副作用——它会贪婪地扩展。onGeometryChange 可以读取相同数据,却不会影响布局。它已回移至 iOS 16。”

“以前尺寸类别一直能用”

**诱惑:**强行通过尺寸类别处理所有情况。

回应:“尺寸类别划分较粗略。iPad 在两种方向下都是 .regular。我会用尺寸类别处理大致分类,并用几何尺寸设定精确阈值。”

“我们不支持 iPad 多任务处理”

诱惑:UIRequiresFullScreen = true

回应:“Apple 已在 iOS 26 弃用仅限全屏模式。即使不主动支持分屏视图,应用也不能在调整大小时出错。基于空间的布局成本相同。”


资源

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

技能:axiom-swiftui-layout-ref、axiom-swiftui-debugging、axiom-liquid-glass

评论

支持 Markdown