关于 axiom-swiftui-layout
面向 SwiftUI 开发者的自适应布局技能,指导界面根据容器可用空间而非设备型号调整。内容涵盖布局工具选择、iPad 多任务场景和 iOS 26 可调整窗口。
主要功能
- 01ViewThatFits、AnyLayout 等工具选择
- 02使用 onGeometryChange 读取尺寸
- 03适配动态字体和尺寸类别
- 04iOS 26 自由调整窗口指导
- 05布局反模式与修正建议
适用场景
- 01让界面适配 iPhone 与 iPad
- 02修复 Split View 或 Stage Manager 中的布局
- 03支持 iOS 26 窗口缩放
- 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 种布局变体中选择 | ViewThatFits | if size > X |
| 以动画在 H↔V 之间切换 | AnyLayout | 条件式 HStack/VStack |
| 读取容器尺寸 | onGeometryChange | GeometryReader |
| 适应辅助功能文本 | dynamicTypeSize | 固定断点 |
| 检测紧凑宽度 | horizontalSizeClass | UIDevice.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