axiom-swiftui-layout-ref
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI Layout API Reference
SwiftUI布局API参考文档
Comprehensive API reference for SwiftUI adaptive layout tools. For decision guidance and anti-patterns, see the skill.
axiom-swiftui-layout这是SwiftUI自适应布局工具的全面API参考文档。如需决策指导和反模式相关内容,请查看技能。
axiom-swiftui-layoutOverview
概述
This reference covers all SwiftUI layout APIs for building adaptive interfaces:
- ViewThatFits — Automatic variant selection (iOS 16+)
- AnyLayout — Type-erased animated layout switching (iOS 16+)
- Layout Protocol — Custom layout algorithms (iOS 16+)
- onGeometryChange — Efficient geometry reading (iOS 16+ backported)
- GeometryReader — Layout-phase geometry access (iOS 13+)
- Safe Area Padding — .safeAreaPadding() vs .padding() (iOS 17+)
- Size Classes — Trait-based adaptation
- iOS 26 Window APIs — Free-form windows, menu bar, resize anchors
本参考文档涵盖了用于构建自适应界面的所有SwiftUI布局API:
- ViewThatFits — 自动变体选择(iOS 16+)
- AnyLayout — 类型擦除的动画布局切换(iOS 16+)
- Layout Protocol — 自定义布局算法(iOS 16+)
- onGeometryChange — 高效的几何信息读取(向后兼容至iOS 16)
- GeometryReader — 布局阶段的几何信息访问(iOS 13+)
- 安全区域边距 — .safeAreaPadding() 与 .padding() 的对比(iOS 17+)
- 尺寸类 — 基于特性的适配
- iOS 26窗口API — 自由形式窗口、菜单栏、调整大小锚点
ViewThatFits
ViewThatFits
Evaluates child views in order and displays the first one that fits in the available space.
按顺序评估子视图,并显示第一个能适配可用空间的视图。
Basic Usage
基本用法
swift
ViewThatFits {
// First choice
HStack {
icon
title
Spacer()
button
}
// Second choice
HStack {
icon
title
button
}
// Fallback
VStack {
HStack { icon; title }
button
}
}swift
ViewThatFits {
// 首选布局
HStack {
icon
title
Spacer()
button
}
// 次选布局
HStack {
icon
title
button
}
// 回退布局
VStack {
HStack { icon; title }
button
}
}With Axis Constraint
带轴约束
swift
// Only consider horizontal fit
ViewThatFits(in: .horizontal) {
wideVersion
narrowVersion
}
// Only consider vertical fit
ViewThatFits(in: .vertical) {
tallVersion
shortVersion
}swift
// 仅考虑水平方向适配
ViewThatFits(in: .horizontal) {
wideVersion
narrowVersion
}
// 仅考虑垂直方向适配
ViewThatFits(in: .vertical) {
tallVersion
shortVersion
}How It Works
工作原理
- Applies to each child
fixedSize() - Measures ideal size against available space
- Returns first child that fits
- Falls back to last child if none fit
- 为每个子视图应用
fixedSize() - 将理想尺寸与可用空间进行比较
- 返回第一个适配的子视图
- 如果没有视图适配,则返回最后一个子视图
Limitations
局限性
- Does not expose which variant was selected
- Cannot animate between variants (use AnyLayout instead)
- Measures all variants (performance consideration for complex views)
- 不会暴露选中了哪个变体
- 无法在变体之间添加动画(请改用AnyLayout)
- 会测量所有变体(复杂视图需考虑性能影响)
AnyLayout
AnyLayout
Type-erased layout container enabling animated transitions between layouts.
类型擦除的布局容器,支持布局之间的动画过渡。
Basic Usage
基本用法
swift
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var sizeClass
var layout: AnyLayout {
sizeClass == .compact
? AnyLayout(VStackLayout(spacing: 12))
: AnyLayout(HStackLayout(spacing: 20))
}
var body: some View {
layout {
ForEach(items) { item in
ItemView(item: item)
}
}
.animation(.default, value: sizeClass)
}
}swift
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var sizeClass
var layout: AnyLayout {
sizeClass == .compact
? AnyLayout(VStackLayout(spacing: 12))
: AnyLayout(HStackLayout(spacing: 20))
}
var body: some View {
layout {
ForEach(items) { item in
ItemView(item: item)
}
}
.animation(.default, value: sizeClass)
}
}Available Layout Types
可用布局类型
swift
AnyLayout(HStackLayout(alignment: .top, spacing: 10))
AnyLayout(VStackLayout(alignment: .leading, spacing: 8))
AnyLayout(ZStackLayout(alignment: .center))
AnyLayout(GridLayout(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 10))swift
AnyLayout(HStackLayout(alignment: .top, spacing: 10))
AnyLayout(VStackLayout(alignment: .leading, spacing: 8))
AnyLayout(ZStackLayout(alignment: .center))
AnyLayout(GridLayout(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 10))Custom Conditions
自定义条件
swift
// Based on Dynamic Type
@Environment(\.dynamicTypeSize) var typeSize
var layout: AnyLayout {
typeSize.isAccessibilitySize
? AnyLayout(VStackLayout())
: AnyLayout(HStackLayout())
}
// Based on geometry
@State private var isWide = true
var layout: AnyLayout {
isWide
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
}swift
// 基于动态字体大小
@Environment(\.dynamicTypeSize) var typeSize
var layout: AnyLayout {
typeSize.isAccessibilitySize
? AnyLayout(VStackLayout())
: AnyLayout(HStackLayout())
}
// 基于几何信息
@State private var isWide = true
var layout: AnyLayout {
isWide
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
}Why Use Over Conditional Views
为何优于条件视图
swift
// ❌ Loses view identity, no animation
if isCompact {
VStack { content }
} else {
HStack { content }
}
// ✅ Preserves identity, smooth animation
let layout = isCompact ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
layout { content }swift
// ❌ 丢失视图标识,无动画效果
if isCompact {
VStack { content }
} else {
HStack { content }
}
// ✅ 保留视图标识,过渡动画流畅
let layout = isCompact ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
layout { content }Layout Protocol
Layout Protocol
Create custom layout containers with full control over positioning.
创建可完全控制定位的自定义布局容器。
Basic Custom Layout
基础自定义布局
swift
struct FlowLayout: Layout {
var spacing: CGFloat = 8
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
return calculateSize(for: sizes, in: proposal.width ?? .infinity)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var point = bounds.origin
var lineHeight: CGFloat = 0
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
if point.x + size.width > bounds.maxX {
point.x = bounds.origin.x
point.y += lineHeight + spacing
lineHeight = 0
}
subview.place(at: point, proposal: .unspecified)
point.x += size.width + spacing
lineHeight = max(lineHeight, size.height)
}
}
}
// Usage
FlowLayout(spacing: 12) {
ForEach(tags) { tag in
TagView(tag: tag)
}
}swift
struct FlowLayout: Layout {
var spacing: CGFloat = 8
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
return calculateSize(for: sizes, in: proposal.width ?? .infinity)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var point = bounds.origin
var lineHeight: CGFloat = 0
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
if point.x + size.width > bounds.maxX {
point.x = bounds.origin.x
point.y += lineHeight + spacing
lineHeight = 0
}
subview.place(at: point, proposal: .unspecified)
point.x += size.width + spacing
lineHeight = max(lineHeight, size.height)
}
}
}
// 使用示例
FlowLayout(spacing: 12) {
ForEach(tags) { tag in
TagView(tag: tag)
}
}With Cache
带缓存的布局
swift
struct CachedLayout: Layout {
struct CacheData {
var sizes: [CGSize] = []
}
func makeCache(subviews: Subviews) -> CacheData {
CacheData(sizes: subviews.map { $0.sizeThatFits(.unspecified) })
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize {
// Use cache.sizes instead of measuring again
}
}swift
struct CachedLayout: Layout {
struct CacheData {
var sizes: [CGSize] = []
}
func makeCache(subviews: Subviews) -> CacheData {
CacheData(sizes: subviews.map { $0.sizeThatFits(.unspecified) })
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize {
// 使用cache.sizes而非再次测量
}
}Layout Values
布局值
swift
// Define custom layout value
struct Rank: LayoutValueKey {
static let defaultValue: Int = 0
}
extension View {
func rank(_ value: Int) -> some View {
layoutValue(key: Rank.self, value: value)
}
}
// Read in layout
func placeSubviews(...) {
let sorted = subviews.sorted { $0[Rank.self] < $1[Rank.self] }
}swift
// 定义自定义布局值
struct Rank: LayoutValueKey {
static let defaultValue: Int = 0
}
extension View {
func rank(_ value: Int) -> some View {
layoutValue(key: Rank.self, value: value)
}
}
// 在布局中读取
func placeSubviews(...) {
let sorted = subviews.sorted { $0[Rank.self] < $1[Rank.self] }
}onGeometryChange
onGeometryChange
Efficient geometry reading without layout side effects. Backported to iOS 16+.
无布局副作用的高效几何信息读取,向后兼容至iOS 16。
Basic Usage
基本用法
swift
@State private var size: CGSize = .zero
var body: some View {
content
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newSize in
size = newSize
}
}swift
@State private var size: CGSize = .zero
var body: some View {
content
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newSize in
size = newSize
}
}Reading Specific Values
读取特定值
swift
// Width only
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { width in
columnCount = max(1, Int(width / 150))
}
// Frame in coordinate space
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { frame in
globalFrame = frame
}
// Aspect ratio
.onGeometryChange(for: Bool.self) { proxy in
proxy.size.width > proxy.size.height
} action: { isWide in
self.isWide = isWide
}swift
// 仅读取宽度
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { width in
columnCount = max(1, Int(width / 150))
}
// 坐标空间中的框架
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { frame in
globalFrame = frame
}
// 宽高比
.onGeometryChange(for: Bool.self) { proxy in
proxy.size.width > proxy.size.height
} action: { isWide in
self.isWide = isWide
}Coordinate Spaces
坐标空间
swift
// Named coordinate space
ScrollView {
content
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .named("scroll")).minY
} action: { offset in
scrollOffset = offset
}
}
.coordinateSpace(name: "scroll")swift
// 命名坐标空间
ScrollView {
content
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .named("scroll")).minY
} action: { offset in
scrollOffset = offset
}
}
.coordinateSpace(name: "scroll")Comparison with GeometryReader
与GeometryReader的对比
| Aspect | onGeometryChange | GeometryReader |
|---|---|---|
| Layout impact | None | Greedy (fills space) |
| When evaluated | After layout | During layout |
| Use case | Side effects | Layout calculations |
| iOS version | 16+ (backported) | 13+ |
| 特性 | onGeometryChange | GeometryReader |
|---|---|---|
| 布局影响 | 无 | 贪婪填充(占满可用空间) |
| 评估时机 | 布局完成后 | 布局过程中 |
| 使用场景 | 副作用处理 | 布局计算 |
| iOS版本 | 16+(向后兼容) | 13+ |
GeometryReader
GeometryReader
Provides geometry information during layout phase. Use sparingly due to greedy sizing.
在布局阶段提供几何信息,因贪婪特性需谨慎使用。
Basic Usage (Constrained)
基础用法(带约束)
swift
// ✅ Always constrain GeometryReader
GeometryReader { proxy in
let width = proxy.size.width
HStack(spacing: 0) {
Rectangle().frame(width: width * 0.3)
Rectangle().frame(width: width * 0.7)
}
}
.frame(height: 100) // Required constraintswift
// ✅ 始终为GeometryReader添加约束
GeometryReader { proxy in
let width = proxy.size.width
HStack(spacing: 0) {
Rectangle().frame(width: width * 0.3)
Rectangle().frame(width: width * 0.7)
}
}
.frame(height: 100) // 必须添加的约束GeometryProxy Properties
GeometryProxy属性
swift
GeometryReader { proxy in
// Container size
let size = proxy.size // CGSize
// Safe area insets
let insets = proxy.safeAreaInsets // EdgeInsets
// Frame in coordinate space
let globalFrame = proxy.frame(in: .global)
let localFrame = proxy.frame(in: .local)
let namedFrame = proxy.frame(in: .named("container"))
}swift
GeometryReader { proxy in
// 容器尺寸
let size = proxy.size // CGSize
// 安全区域内边距
let insets = proxy.safeAreaInsets // EdgeInsets
// 坐标空间中的框架
let globalFrame = proxy.frame(in: .global)
let localFrame = proxy.frame(in: .local)
let namedFrame = proxy.frame(in: .named("container"))
}Common Patterns
常见模式
swift
// Proportional sizing
GeometryReader { geo in
VStack {
header.frame(height: geo.size.height * 0.2)
content.frame(height: geo.size.height * 0.8)
}
}
// Centering with offset
GeometryReader { geo in
content
.position(x: geo.size.width / 2, y: geo.size.height / 2)
}swift
// 比例尺寸
GeometryReader { geo in
VStack {
header.frame(height: geo.size.height * 0.2)
content.frame(height: geo.size.height * 0.8)
}
}
// 偏移居中
GeometryReader { geo in
content
.position(x: geo.size.width / 2, y: geo.size.height / 2)
}Avoiding Common Mistakes
避免常见错误
swift
// ❌ Unconstrained in VStack
VStack {
GeometryReader { ... } // Takes ALL space
Button("Next") { } // Invisible
}
// ✅ Constrained
VStack {
GeometryReader { ... }
.frame(height: 200)
Button("Next") { }
}
// ❌ Causing layout loops
GeometryReader { geo in
content
.frame(width: geo.size.width) // Can cause infinite loop
}swift
// ❌ VStack中未添加约束
VStack {
GeometryReader { ... } // 占据所有可用空间
Button("下一步") { } // 不可见
}
// ✅ 添加约束
VStack {
GeometryReader { ... }
.frame(height: 200)
Button("下一步") { }
}
// ❌ 导致布局循环
GeometryReader { geo in
content
.frame(width: geo.size.width) // 可能引发无限循环
}Safe Area Padding
安全区域边距
SwiftUI provides two primary approaches for handling spacing around content: and . Understanding when to use each is critical for proper layout on devices with safe areas (notch, Dynamic Island, home indicator).
.padding().safeAreaPadding()SwiftUI提供两种处理内容周围间距的主要方式:和。理解两者的使用场景对于在带有安全区域(刘海、灵动岛、Home指示器)的设备上实现正确布局至关重要。
.padding().safeAreaPadding()The Critical Difference
核心差异
swift
// ❌ WRONG - Ignores safe areas, content hits notch/home indicator
ScrollView {
content
}
.padding(.horizontal, 20)
// ✅ CORRECT - Respects safe areas, adds padding beyond them
ScrollView {
content
}
.safeAreaPadding(.horizontal, 20)Key insight: adds fixed spacing from the view's edges. adds spacing beyond the safe area insets.
.padding().safeAreaPadding()swift
// ❌ 错误示例 - 忽略安全区域,内容会被刘海/Home指示器遮挡
ScrollView {
content
}
.padding(.horizontal, 20)
// ✅ 正确示例 - 尊重安全区域,在安全区域外添加边距
ScrollView {
content
}
.safeAreaPadding(.horizontal, 20)核心要点:从视图边缘添加固定间距,在安全区域内边距之外添加间距。
.padding().safeAreaPadding()When to Use Each
使用场景
Use .padding()
when
.padding()使用.padding()
的场景
.padding()- Adding spacing between sibling views within a container
- Creating internal spacing that should be consistent everywhere
- Working with views that already respect safe areas (like List, Form)
- Adding decorative spacing on macOS (no safe area concerns)
swift
VStack(spacing: 0) {
header
.padding(.horizontal, 16) // ✅ Internal spacing
Divider()
content
.padding(.horizontal, 16) // ✅ Internal spacing
}- 在容器内为兄弟视图添加间距
- 创建在所有场景下保持一致的内部间距
- 与已支持安全区域的视图(如List、Form)配合使用
- 在macOS上添加装饰性间距(无安全区域问题)
swift
VStack(spacing: 0) {
header
.padding(.horizontal, 16) // ✅ 内部间距
Divider()
content
.padding(.horizontal, 16) // ✅ 内部间距
}Use .safeAreaPadding()
when (iOS 17+)
.safeAreaPadding()使用.safeAreaPadding()
的场景(iOS 17+)
.safeAreaPadding()- Adding margin to full-width content that extends to screen edges
- Implementing edge-to-edge scrolling with proper insets
- Creating custom containers that need safe area awareness
- Working with Liquid Glass or full-screen materials
swift
// ✅ Edge-to-edge list with custom padding
List(items) { item in
ItemRow(item)
}
.listStyle(.plain)
.safeAreaPadding(.horizontal, 20) // Adds 20pt beyond safe areas
// ✅ Full-screen content with proper margins
ZStack {
Color.blue.ignoresSafeArea()
VStack {
content
}
.safeAreaPadding(.all, 16) // Respects notch, home indicator
}- 为延伸至屏幕边缘的全屏内容添加边距
- 实现带正确内边距的边缘到边缘滚动
- 创建需要感知安全区域的自定义容器
- 配合Liquid Glass或全屏材质使用
swift
// ✅ 带自定义边距的边缘到边缘列表
List(items) { item in
ItemRow(item)
}
.listStyle(.plain)
.safeAreaPadding(.horizontal, 20) // 在安全区域外添加20pt边距
// ✅ 带正确边距的全屏内容
ZStack {
Color.blue.ignoresSafeArea()
VStack {
content
}
.safeAreaPadding(.all, 16) // 避开刘海、Home指示器
}Platform Availability
平台兼容性
iOS 17+, iPadOS 17+, macOS 14+, axiom-visionOS 1.0+
For earlier iOS versions, use manual safe area handling:
swift
// iOS 13-16 fallback
GeometryReader { geo in
content
.padding(.horizontal, 20 + geo.safeAreaInsets.leading)
}Or conditional compilation:
swift
if #available(iOS 17, *) {
content.safeAreaPadding(.horizontal, 20)
} else {
content.padding(.horizontal, 20)
.padding(.leading, safeAreaInsets.leading)
}iOS 17+, iPadOS 17+, macOS 14+, axiom-visionOS 1.0+
对于早期iOS版本,使用手动安全区域处理:
swift
// iOS 13-16兼容方案
GeometryReader { geo in
content
.padding(.horizontal, 20 + geo.safeAreaInsets.leading)
}或条件编译:
swift
if #available(iOS 17, *) {
content.safeAreaPadding(.horizontal, 20)
} else {
content.padding(.horizontal, 20)
.padding(.leading, safeAreaInsets.leading)
}Edge-Specific Usage
特定边缘用法
swift
// Top only (below status bar/notch)
.safeAreaPadding(.top, 8)
// Bottom only (above home indicator)
.safeAreaPadding(.bottom, 16)
// Horizontal (left/right of safe areas)
.safeAreaPadding(.horizontal, 20)
// All edges
.safeAreaPadding(.all, 16)
// Individual edges
.safeAreaPadding(EdgeInsets(top: 8, leading: 20, bottom: 16, trailing: 20))swift
// 仅顶部(状态栏/刘海下方)
.safeAreaPadding(.top, 8)
// 仅底部(Home指示器上方)
.safeAreaPadding(.bottom, 16)
// 水平方向(安全区域左右两侧)
.safeAreaPadding(.horizontal, 20)
// 所有边缘
.safeAreaPadding(.all, 16)
// 单独设置各边缘
.safeAreaPadding(EdgeInsets(top: 8, leading: 20, bottom: 16, trailing: 20))Common Patterns
常见模式
Edge-to-Edge ScrollView
边缘到边缘滚动视图
swift
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemCard(item)
}
}
}
.safeAreaPadding(.horizontal, 16) // Content inset from edges + safe areas
.safeAreaPadding(.vertical, 8)swift
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemCard(item)
}
}
}
.safeAreaPadding(.horizontal, 16) // 内容与屏幕边缘+安全区域保持内边距
.safeAreaPadding(.vertical, 8)Full-Screen Background with Safe Content
全屏背景与安全内容
swift
ZStack {
// Background extends edge-to-edge
LinearGradient(...)
.ignoresSafeArea()
// Content respects safe areas + custom padding
VStack {
header
Spacer()
content
Spacer()
footer
}
.safeAreaPadding(.all, 20)
}swift
ZStack {
// 背景延伸至屏幕边缘
LinearGradient(...)
.ignoresSafeArea()
// 内容尊重安全区域并添加自定义边距
VStack {
header
Spacer()
content
Spacer()
footer
}
.safeAreaPadding(.all, 20)
}Nested Padding (Combined Approach)
嵌套边距(组合用法)
swift
// Outer: Safe area padding for device insets
VStack(spacing: 0) {
content
}
.safeAreaPadding(.horizontal, 16) // Beyond safe areas
// Inner: Regular padding for internal spacing
VStack {
Text("Title")
.padding(.bottom, 8) // Internal spacing
Text("Subtitle")
}swift
// 外层:为设备安全区域添加边距
VStack(spacing: 0) {
content
}
.safeAreaPadding(.horizontal, 16) // 在安全区域外添加边距
// 内层:为内部元素添加常规边距
VStack {
Text("标题")
.padding(.bottom, 8) // 内部间距
Text("副标题")
}Decision Tree
决策树
Does your content extend to screen edges?
├─ YES → Use .safeAreaPadding()
│ ├─ Is it scrollable? → .safeAreaPadding(.horizontal/.vertical)
│ └─ Is it full-screen? → .safeAreaPadding(.all)
│
└─ NO (contained within a safe container like List/Form)
└─ Use .padding() for internal spacing你的内容是否延伸至屏幕边缘?
├─ 是 → 使用.safeAreaPadding()
│ ├─ 是否为滚动视图? → .safeAreaPadding(.horizontal/.vertical)
│ └─ 是否为全屏内容? → .safeAreaPadding(.all)
│
└─ 否(包含在List/Form等安全容器内)
└─ 使用.padding()添加内部间距Visual Debugging
可视化调试
swift
// Visualize safe area padding (iOS 17+)
content
.safeAreaPadding(.horizontal, 20)
.background(.red.opacity(0.2)) // Shows padding area
.border(.blue) // Shows content boundsswift
// 可视化安全区域边距(iOS 17+)
content
.safeAreaPadding(.horizontal, 20)
.background(.red.opacity(0.2)) // 显示边距区域
.border(.blue) // 显示内容边界Migration from Manual Safe Area Handling
从手动安全区域处理迁移
swift
// ❌ OLD: Manual calculation (iOS 13-16)
GeometryReader { geo in
content
.padding(.top, geo.safeAreaInsets.top + 16)
.padding(.bottom, geo.safeAreaInsets.bottom + 16)
.padding(.horizontal, 20)
}
// ✅ NEW: .safeAreaPadding() (iOS 17+)
content
.safeAreaPadding(.vertical, 16)
.safeAreaPadding(.horizontal, 20)swift
// ❌ 旧方案:手动计算(iOS 13-16)
GeometryReader { geo in
content
.padding(.top, geo.safeAreaInsets.top + 16)
.padding(.bottom, geo.safeAreaInsets.bottom + 16)
.padding(.horizontal, 20)
}
// ✅ 新方案:.safeAreaPadding()(iOS 17+)
content
.safeAreaPadding(.vertical, 16)
.safeAreaPadding(.horizontal, 20)Related APIs
相关API
.safeAreaInset(edge:)swift
ScrollView {
content
}
.safeAreaInset(edge: .bottom) {
// This REDUCES the safe area, content scrolls under it
toolbarButtons
.padding()
.background(.ultraThinMaterial)
}.ignoresSafeArea()swift
Color.blue
.ignoresSafeArea() // Extends to absolute screen edges.safeAreaInset(edge:)swift
ScrollView {
content
}
.safeAreaInset(edge: .bottom) {
// 此内容会缩小安全区域,其他内容可在其下方滚动
toolbarButtons
.padding()
.background(.ultraThinMaterial)
}.ignoresSafeArea()swift
Color.blue
.ignoresSafeArea() // 延伸至屏幕绝对边缘Why It Matters
重要性
Before iOS 17: Developers had to manually calculate safe area insets with GeometryReader, leading to:
- Verbose code
- Performance overhead (GeometryReader forces extra layout pass)
- Easy mistakes (forgetting to check all edges)
iOS 17+: provides:
.safeAreaPadding()- Declarative API (matches SwiftUI philosophy)
- Automatic safe area awareness
- Better performance (no extra layout passes)
- Type-safe edge specification
Real-world impact: Using instead of on iPhone 15 Pro causes content to:
.padding().safeAreaPadding()- Hit the Dynamic Island (top)
- Overlap the home indicator (bottom)
- Get cut off by screen corners (rounded edges)
iOS 17之前:开发者必须使用GeometryReader手动计算安全区域内边距,导致:
- 代码冗长
- 性能开销(GeometryReader会强制额外的布局传递)
- 容易出错(忘记检查所有边缘)
iOS 17+:提供:
.safeAreaPadding()- 声明式API(符合SwiftUI设计理念)
- 自动感知安全区域
- 更好的性能(无额外布局传递)
- 类型安全的边缘指定
实际影响:在iPhone 15 Pro上使用而非会导致:
.padding().safeAreaPadding()- 内容被灵动岛遮挡(顶部)
- 内容与Home指示器重叠(底部)
- 内容被屏幕圆角截断
Size Classes
尺寸类
Environment values indicating horizontal and vertical size characteristics.
表示水平和垂直尺寸特性的环境值。
Reading Size Classes
读取尺寸类
swift
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
if horizontalSizeClass == .compact {
compactLayout
} else {
regularLayout
}
}
}swift
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
if horizontalSizeClass == .compact {
compactLayout
} else {
regularLayout
}
}
}Size Class Values
尺寸类取值
swift
enum UserInterfaceSizeClass {
case compact // Constrained space
case regular // Ample space
}swift
enum UserInterfaceSizeClass {
case compact // 空间受限
case regular // 空间充足
}Platform Behavior
平台表现
iPhone:
| Orientation | Horizontal | Vertical |
|---|---|---|
| Portrait | | |
| Landscape (small) | | |
| Landscape (Plus/Max) | | |
iPad:
| Configuration | Horizontal | Vertical |
|---|---|---|
| Any full screen | | |
| 70% Split View | | |
| 50% Split View | | |
| 33% Split View | | |
| Slide Over | | |
iPhone:
| 方向 | 水平 | 垂直 |
|---|---|---|
| 竖屏 | | |
| 横屏(小屏机型) | | |
| 横屏(Plus/Max机型) | | |
iPad:
| 配置 | 水平 | 垂直 |
|---|---|---|
| 全屏 | | |
| 70%分屏 | | |
| 50%分屏 | | |
| 33%分屏 | | |
| 侧滑 | | |
Overriding Size Classes
覆盖尺寸类
swift
content
.environment(\.horizontalSizeClass, .compact)swift
content
.environment(\.horizontalSizeClass, .compact)Dynamic Type Size
动态字体大小
Environment value for user's preferred text size.
用户偏好文本大小的环境值。
Reading Dynamic Type
读取动态字体大小
swift
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if dynamicTypeSize.isAccessibilitySize {
accessibleLayout
} else {
standardLayout
}
}swift
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if dynamicTypeSize.isAccessibilitySize {
accessibleLayout
} else {
standardLayout
}
}Size Categories
尺寸类别
swift
enum DynamicTypeSize: Comparable {
case xSmall
case small
case medium
case large // Default
case xLarge
case xxLarge
case xxxLarge
case accessibility1 // isAccessibilitySize = true
case accessibility2
case accessibility3
case accessibility4
case accessibility5
}swift
enum DynamicTypeSize: Comparable {
case xSmall
case small
case medium
case large // 默认
case xLarge
case xxLarge
case xxxLarge
case accessibility1 // isAccessibilitySize = true
case accessibility2
case accessibility3
case accessibility4
case accessibility5
}Scaled Metric
缩放度量
swift
@ScaledMetric var iconSize: CGFloat = 24
@ScaledMetric(relativeTo: .largeTitle) var headerSize: CGFloat = 44
Image(systemName: "star")
.frame(width: iconSize, height: iconSize)swift
@ScaledMetric var iconSize: CGFloat = 24
@ScaledMetric(relativeTo: .largeTitle) var headerSize: CGFloat = 44
Image(systemName: "star")
.frame(width: iconSize, height: iconSize)iOS 26 Window APIs
iOS 26窗口API
Window Resize Anchor
窗口调整大小锚点
swift
WindowGroup {
ContentView()
}
.windowResizeAnchor(.topLeading) // Resize originates from top-left
.windowResizeAnchor(.center) // Resize from centerswift
WindowGroup {
ContentView()
}
.windowResizeAnchor(.topLeading) // 从左上角开始调整大小
.windowResizeAnchor(.center) // 从中心开始调整大小Menu Bar Commands (iPad)
菜单栏命令(iPad)
swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("View") {
Button("Show Sidebar") {
showSidebar.toggle()
}
.keyboardShortcut("s", modifiers: [.command, .option])
Divider()
Button("Zoom In") { zoom += 0.1 }
.keyboardShortcut("+")
Button("Zoom Out") { zoom -= 0.1 }
.keyboardShortcut("-")
}
}
}
}swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("视图") {
Button("显示侧边栏") {
showSidebar.toggle()
}
.keyboardShortcut("s", modifiers: [.command, .option])
Divider()
Button("放大") { zoom += 0.1 }
.keyboardShortcut("+")
Button("缩小") { zoom -= 0.1 }
.keyboardShortcut("-")
}
}
}
}NavigationSplitView Column Control
导航分栏视图列控制
swift
// iOS 26: Automatic column visibility
NavigationSplitView {
Sidebar()
} content: {
ContentList()
} detail: {
DetailView()
}
// Columns auto-hide/show based on available width
// Manual control (when needed)
@State private var columnVisibility: NavigationSplitViewVisibility = .all
NavigationSplitView(columnVisibility: $columnVisibility) {
Sidebar()
} detail: {
DetailView()
}swift
// iOS 26:自动列可见性
NavigationSplitView {
Sidebar()
} content: {
ContentList()
} detail: {
DetailView()
}
// 列会根据可用宽度自动隐藏/显示
// 手动控制(必要时)
@State private var columnVisibility: NavigationSplitViewVisibility = .all
NavigationSplitView(columnVisibility: $columnVisibility) {
Sidebar()
} detail: {
DetailView()
}Scene Phase
场景阶段
swift
@Environment(\.scenePhase) var scenePhase
var body: some View {
content
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
case .active:
// Window is visible and interactive
case .inactive:
// Window is visible but not interactive
case .background:
// Window is not visible
}
}
}swift
@Environment(\.scenePhase) var scenePhase
var body: some View {
content
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
case .active:
// 窗口可见且可交互
case .inactive:
// 窗口可见但不可交互
case .background:
// 窗口不可见
}
}
}Coordinate Spaces
坐标空间
Built-in Coordinate Spaces
内置坐标空间
swift
// Global (screen coordinates)
proxy.frame(in: .global)
// Local (view's own bounds)
proxy.frame(in: .local)
// Named (custom)
proxy.frame(in: .named("mySpace"))swift
// 全局(屏幕坐标)
proxy.frame(in: .global)
// 本地(视图自身边界)
proxy.frame(in: .local)
// 命名(自定义)
proxy.frame(in: .named("mySpace"))Creating Named Spaces
创建命名空间
swift
ScrollView {
content
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .named("scroll")).minY
} action: { offset in
scrollOffset = offset
}
}
.coordinateSpace(name: "scroll")
// iOS 17+ typed coordinate space
extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace {
static var scroll: Self { .named("scroll") }
}swift
ScrollView {
content
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .named("scroll")).minY
} action: { offset in
scrollOffset = offset
}
}
.coordinateSpace(name: "scroll")
// iOS 17+ 类型化坐标空间
extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace {
static var scroll: Self { .named("scroll") }
}ScrollView Geometry (iOS 18+)
滚动视图几何信息(iOS 18+)
onScrollGeometryChange
onScrollGeometryChange
swift
ScrollView {
content
}
.onScrollGeometryChange(for: CGFloat.self) { geometry in
geometry.contentOffset.y
} action: { offset in
scrollOffset = offset
}swift
ScrollView {
content
}
.onScrollGeometryChange(for: CGFloat.self) { geometry in
geometry.contentOffset.y
} action: { offset in
scrollOffset = offset
}ScrollGeometry Properties
ScrollGeometry属性
swift
.onScrollGeometryChange(for: ScrollGeometry.self) { $0 } action: { geo in
let offset = geo.contentOffset // Current scroll position
let size = geo.contentSize // Total content size
let visible = geo.visibleRect // Currently visible rect
let insets = geo.contentInsets // Content insets
}swift
.onScrollGeometryChange(for: ScrollGeometry.self) { $0 } action: { geo in
let offset = geo.contentOffset // 当前滚动位置
let size = geo.contentSize // 总内容尺寸
let visible = geo.visibleRect // 当前可见区域
let insets = geo.contentInsets // 内容内边距
}Resources
资源
WWDC: 2025-208, 2024-10074, 2022-10056
Docs: /swiftui/layout, /swiftui/viewthatfits
Skills: axiom-swiftui-layout, axiom-swiftui-debugging
WWDC:2025-208, 2024-10074, 2022-10056
文档:/swiftui/layout, /swiftui/viewthatfits
技能:axiom-swiftui-layout, axiom-swiftui-debugging