axiom-swiftui-layout-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Layout API Reference

SwiftUI布局API参考文档

Comprehensive API reference for SwiftUI adaptive layout tools. For decision guidance and anti-patterns, see the
axiom-swiftui-layout
skill.
这是SwiftUI自适应布局工具的全面API参考文档。如需决策指导和反模式相关内容,请查看
axiom-swiftui-layout
技能。

Overview

概述

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

工作原理

  1. Applies
    fixedSize()
    to each child
  2. Measures ideal size against available space
  3. Returns first child that fits
  4. Falls back to last child if none fit
  1. 为每个子视图应用
    fixedSize()
  2. 将理想尺寸与可用空间进行比较
  3. 返回第一个适配的子视图
  4. 如果没有视图适配,则返回最后一个子视图

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的对比

AspectonGeometryChangeGeometryReader
Layout impactNoneGreedy (fills space)
When evaluatedAfter layoutDuring layout
Use caseSide effectsLayout calculations
iOS version16+ (backported)13+

特性onGeometryChangeGeometryReader
布局影响贪婪填充(占满可用空间)
评估时机布局完成后布局过程中
使用场景副作用处理布局计算
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 constraint
swift
// ✅ 始终为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:
.padding()
and
.safeAreaPadding()
. Understanding when to use each is critical for proper layout on devices with safe areas (notch, Dynamic Island, home indicator).
SwiftUI提供两种处理内容周围间距的主要方式:
.padding()
.safeAreaPadding()
。理解两者的使用场景对于在带有安全区域(刘海、灵动岛、Home指示器)的设备上实现正确布局至关重要。

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:
.padding()
adds fixed spacing from the view's edges.
.safeAreaPadding()
adds spacing beyond the safe area insets.
swift
// ❌ 错误示例 - 忽略安全区域,内容会被刘海/Home指示器遮挡
ScrollView {
    content
}
.padding(.horizontal, 20)

// ✅ 正确示例 - 尊重安全区域,在安全区域外添加边距
ScrollView {
    content
}
.safeAreaPadding(.horizontal, 20)
核心要点
.padding()
从视图边缘添加固定间距,
.safeAreaPadding()
在安全区域内边距之外添加间距。

When to Use Each

使用场景

Use
.padding()
when

使用
.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()
的场景(iOS 17+)

  • 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 bounds
swift
// 可视化安全区域边距(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:)
- Adds persistent content that shrinks the safe area:
swift
ScrollView {
    content
}
.safeAreaInset(edge: .bottom) {
    // This REDUCES the safe area, content scrolls under it
    toolbarButtons
        .padding()
        .background(.ultraThinMaterial)
}
.ignoresSafeArea()
- Opts out of safe area completely:
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+:
.safeAreaPadding()
provides:
  • Declarative API (matches SwiftUI philosophy)
  • Automatic safe area awareness
  • Better performance (no extra layout passes)
  • Type-safe edge specification
Real-world impact: Using
.padding()
instead of
.safeAreaPadding()
on iPhone 15 Pro causes content to:
  • 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:
OrientationHorizontalVertical
Portrait
.compact
.regular
Landscape (small)
.compact
.compact
Landscape (Plus/Max)
.regular
.compact
iPad:
ConfigurationHorizontalVertical
Any full screen
.regular
.regular
70% Split View
.regular
.regular
50% Split View
.regular
.regular
33% Split View
.compact
.regular
Slide Over
.compact
.regular
iPhone:
方向水平垂直
竖屏
.compact
.regular
横屏(小屏机型)
.compact
.compact
横屏(Plus/Max机型)
.regular
.compact
iPad:
配置水平垂直
全屏
.regular
.regular
70%分屏
.regular
.regular
50%分屏
.regular
.regular
33%分屏
.compact
.regular
侧滑
.compact
.regular

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 center
swift
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