swiftui-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Patterns

SwiftUI 模式

Modern SwiftUI patterns targeting iOS 26+ with Swift 6.2. Covers architecture, state management, navigation, view composition, and component usage. Patterns are backward-compatible to iOS 17 unless noted.
面向iOS 26+、基于Swift 6.2的现代SwiftUI模式,涵盖架构、状态管理、导航、视图组合和组件使用。除非特别说明,所有模式均向下兼容至iOS 17。

Architecture: Model-View (MV) Pattern

架构:模型-视图(MV)模式

Default to MV -- views are lightweight state expressions; models and services own business logic. Do not introduce view models unless the existing code already uses them.
Core principles:
  • Favor
    @State
    ,
    @Environment
    ,
    @Query
    ,
    .task
    , and
    .onChange
    for orchestration
  • Inject services and shared models via
    @Environment
    ; keep views small and composable
  • Split large views into smaller subviews rather than introducing a view model
  • Test models, services, and business logic; keep views simple and declarative
swift
struct FeedView: View {
    @Environment(FeedClient.self) private var client

    enum ViewState {
        case loading, error(String), loaded([Post])
    }

    @State private var viewState: ViewState = .loading

    var body: some View {
        List {
            switch viewState {
            case .loading:
                ProgressView()
            case .error(let message):
                ContentUnavailableView("Error", systemImage: "exclamationmark.triangle",
                                       description: Text(message))
            case .loaded(let posts):
                ForEach(posts) { post in
                    PostRow(post: post)
                }
            }
        }
        .task { await loadFeed() }
        .refreshable { await loadFeed() }
    }

    private func loadFeed() async {
        do {
            let posts = try await client.getFeed()
            viewState = .loaded(posts)
        } catch {
            viewState = .error(error.localizedDescription)
        }
    }
}
For MV pattern rationale and extended examples, see
references/mv-patterns.md
.
默认优先使用MV模式:视图是轻量级的状态表达,业务逻辑由模型和服务负责。除非现有代码已经使用了视图模型,否则不要额外引入。
核心原则:
  • 优先使用
    @State
    @Environment
    @Query
    .task
    .onChange
    进行编排
  • 通过
    @Environment
    注入服务和共享模型,保持视图小巧可组合
  • 将大型视图拆分为更小的子视图,而不是引入视图模型
  • 针对模型、服务和业务逻辑编写测试,保持视图简洁声明式
swift
struct FeedView: View {
    @Environment(FeedClient.self) private var client

    enum ViewState {
        case loading, error(String), loaded([Post])
    }

    @State private var viewState: ViewState = .loading

    var body: some View {
        List {
            switch viewState {
            case .loading:
                ProgressView()
            case .error(let message):
                ContentUnavailableView("Error", systemImage: "exclamationmark.triangle",
                                       description: Text(message))
            case .loaded(let posts):
                ForEach(posts) { post in
                    PostRow(post: post)
                }
            }
        }
        .task { await loadFeed() }
        .refreshable { await loadFeed() }
    }

    private func loadFeed() async {
        do {
            let posts = try await client.getFeed()
            viewState = .loaded(posts)
        } catch {
            viewState = .error(error.localizedDescription)
        }
    }
}
如需了解MV模式的设计原理和更多示例,请查看
references/mv-patterns.md

State Management

状态管理

@Observable Ownership Rules

@Observable 所有权规则

Important: Always annotate
@Observable
view model classes with
@MainActor
to ensure UI-bound state is updated on the main thread. Required for Swift 6 concurrency safety.
WrapperWhen to Use
@State
View owns the object or value. Creates and manages lifecycle.
let
View receives an
@Observable
object. Read-only observation -- no wrapper needed.
@Bindable
View receives an
@Observable
object and needs two-way bindings (
$property
).
@Environment(Type.self)
Access shared
@Observable
object from environment.
@State
(value types)
View-local simple state: toggles, counters, text field values. Always
private
.
@Binding
Two-way connection to parent's
@State
or
@Bindable
property.
重要提示: 所有
@Observable
视图模型类都需要添加
@MainActor
注解,确保UI相关状态在主线程更新,这是Swift 6并发安全的强制要求。
包装器适用场景
@State
视图持有该对象或值,负责创建和管理其生命周期。
let
视图接收一个
@Observable
对象,仅需只读观测,无需包装器。
@Bindable
视图接收一个
@Observable
对象,需要双向绑定(
$property
语法)。
@Environment(Type.self)
从环境中访问共享的
@Observable
对象。
@State
(值类型)
视图本地的简单状态:开关、计数器、文本框输入值,始终声明为
private
@Binding
与父级的
@State
@Bindable
属性建立双向连接。

Ownership Pattern

所有权模式

swift
// @Observable view model -- always @MainActor
@MainActor
@Observable final class ItemStore {
    var title = ""
    var items: [Item] = []
}

// View that OWNS the model
struct ParentView: View {
    @State var viewModel = ItemStore()

    var body: some View {
        ChildView(store: viewModel)
            .environment(viewModel)
    }
}

// View that READS (no wrapper needed for @Observable)
struct ChildView: View {
    let store: ItemStore

    var body: some View { Text(store.title) }
}

// View that BINDS (needs two-way access)
struct EditView: View {
    @Bindable var store: ItemStore

    var body: some View {
        TextField("Title", text: $store.title)
    }
}

// View that reads from ENVIRONMENT
struct DeepView: View {
    @Environment(ItemStore.self) var store

    var body: some View {
        @Bindable var s = store
        TextField("Title", text: $s.title)
    }
}
Granular tracking: SwiftUI only re-renders views that read properties that changed. If a view reads
items
but not
isLoading
, changing
isLoading
does not trigger a re-render. This is a major performance advantage over
ObservableObject
.
swift
// @Observable view model -- always @MainActor
@MainActor
@Observable final class ItemStore {
    var title = ""
    var items: [Item] = []
}

// View that OWNS the model
struct ParentView: View {
    @State var viewModel = ItemStore()

    var body: some View {
        ChildView(store: viewModel)
            .environment(viewModel)
    }
}

// View that READS (no wrapper needed for @Observable)
struct ChildView: View {
    let store: ItemStore

    var body: some View { Text(store.title) }
}

// View that BINDS (needs two-way access)
struct EditView: View {
    @Bindable var store: ItemStore

    var body: some View {
        TextField("Title", text: $store.title)
    }
}

// View that reads from ENVIRONMENT
struct DeepView: View {
    @Environment(ItemStore.self) var store

    var body: some View {
        @Bindable var s = store
        TextField("Title", text: $s.title)
    }
}
细粒度更新: SwiftUI仅会重新渲染读取了发生变更属性的视图。如果一个视图仅读取了
items
而没有读取
isLoading
,那么修改
isLoading
不会触发该视图重绘,这是相比
ObservableObject
的核心性能优势。

Legacy ObservableObject

遗留ObservableObject

Only use if supporting iOS 16 or earlier.
@StateObject
@State
,
@ObservedObject
let
,
@EnvironmentObject
@Environment(Type.self)
.
仅当需要支持iOS 16或更低版本时使用,对应替换规则:
@StateObject
@State
@ObservedObject
let
@EnvironmentObject
@Environment(Type.self)

View Ordering Convention

视图成员排序约定

Order members top to bottom within a view struct:
  1. @Environment
    properties
  2. private
    /
    public
    let
    properties
  3. @State
    and other stored properties
  4. Computed
    var
    (non-view)
  5. init
  6. body
  7. Computed view builders and view helpers
  8. Helper and async functions
视图结构体内部的成员按从上到下的顺序排列:
  1. @Environment
    属性
  2. private
    /
    public
    let
    属性
  3. @State
    和其他存储属性
  4. 非视图类型的计算
    var
  5. init
    构造函数
  6. body
    属性
  7. 计算视图构造器和视图辅助方法
  8. 工具方法和异步函数

View Composition

视图组合

Extract Subviews

提取子视图

Break views into focused subviews. Each should have a single responsibility.
swift
var body: some View {
    VStack {
        HeaderSection(title: title, isPinned: isPinned)
        DetailsSection(details: details)
        ActionsSection(onSave: onSave, onCancel: onCancel)
    }
}
将视图拆分为职责单一的聚焦子视图,每个子视图仅负责一项功能。
swift
var body: some View {
    VStack {
        HeaderSection(title: title, isPinned: isPinned)
        DetailsSection(details: details)
        ActionsSection(onSave: onSave, onCancel: onCancel)
    }
}

Computed View Properties

计算视图属性

Keep related subviews as computed properties in the same file; extract to a standalone
View
struct when reuse is intended or the subview carries its own state.
swift
var body: some View {
    List {
        header
        filters
        results
    }
}

private var header: some View {
    VStack(alignment: .leading, spacing: 6) {
        Text(title).font(.title2)
        Text(subtitle).font(.subheadline)
    }
}
将相关的子视图作为计算属性放在同一个文件中;如果需要复用或者子视图自带独立状态,则提取为独立的
View
结构体。
swift
var body: some View {
    List {
        header
        filters
        results
    }
}

private var header: some View {
    VStack(alignment: .leading, spacing: 6) {
        Text(title).font(.title2)
        Text(subtitle).font(.subheadline)
    }
}

ViewBuilder Functions

ViewBuilder 函数

For conditional logic that does not warrant a separate struct:
swift
@ViewBuilder
private func statusBadge(for status: Status) -> some View {
    switch status {
    case .active: Text("Active").foregroundStyle(.green)
    case .inactive: Text("Inactive").foregroundStyle(.secondary)
    }
}
适用于不需要单独封装结构体的条件逻辑场景:
swift
@ViewBuilder
private func statusBadge(for status: Status) -> some View {
    switch status {
    case .active: Text("Active").foregroundStyle(.green)
    case .inactive: Text("Inactive").foregroundStyle(.secondary)
    }
}

Custom View Modifiers

自定义View Modifier

Extract repeated styling into
ViewModifier
:
swift
struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.background)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .shadow(radius: 2)
    }
}
extension View { func cardStyle() -> some View { modifier(CardStyle()) } }
将重复的样式逻辑提取为
ViewModifier
swift
struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.background)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .shadow(radius: 2)
    }
}
extension View { func cardStyle() -> some View { modifier(CardStyle()) } }

Stable View Tree

稳定的视图树

Avoid top-level conditional view swapping. Prefer a single stable base view with conditions inside sections or modifiers (
overlay
,
opacity
,
disabled
,
toolbar
).
避免在顶层使用条件判断切换视图,优先使用单个稳定的基础视图,在内部的区块或修饰符(
overlay
opacity
disabled
toolbar
)中使用条件逻辑。

Large File Handling

大文件处理

When a view file exceeds ~300 lines, split with extensions and
// MARK: -
comments.
当视图文件超过约300行时,使用扩展和
// MARK: -
注释进行拆分。

Navigation

导航

NavigationStack (Push Navigation)

NavigationStack(压栈导航)

swift
struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: item) {
                    ItemRow(item: item)
                }
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
            .navigationTitle("Items")
        }
    }
}
Programmatic navigation:
swift
path.append(item)        // Push
path.removeLast()        // Pop one
path = NavigationPath()  // Pop to root
swift
struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: item) {
                    ItemRow(item: item)
                }
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
            .navigationTitle("Items")
        }
    }
}
编程式导航:
swift
path.append(item)        // 压入新页面
path.removeLast()        // 弹出上一个页面
path = NavigationPath()  // 弹出到根页面

NavigationSplitView (Multi-Column)

NavigationSplitView(多列导航)

swift
struct MasterDetailView: View {
    @State private var selectedItem: Item?

    var body: some View {
        NavigationSplitView {
            List(items, selection: $selectedItem) { item in
                NavigationLink(value: item) { ItemRow(item: item) }
            }
            .navigationTitle("Items")
        } detail: {
            if let item = selectedItem {
                ItemDetailView(item: item)
            } else {
                ContentUnavailableView("Select an Item", systemImage: "sidebar.leading")
            }
        }
    }
}
swift
struct MasterDetailView: View {
    @State private var selectedItem: Item?

    var body: some View {
        NavigationSplitView {
            List(items, selection: $selectedItem) { item in
                NavigationLink(value: item) { ItemRow(item: item) }
            }
            .navigationTitle("Items")
        } detail: {
            if let item = selectedItem {
                ItemDetailView(item: item)
            } else {
                ContentUnavailableView("Select an Item", systemImage: "sidebar.leading")
            }
        }
    }
}

Sheet Presentation

Sheet 弹窗

Prefer
.sheet(item:)
over
.sheet(isPresented:)
when state represents a selected model. Sheets should own their actions and call
dismiss()
internally.
swift
@State private var selectedItem: Item?

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
}
Presentation sizing (iOS 18+): Control sheet dimensions with
.presentationSizing
:
swift
.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .presentationSizing(.form)  // .form, .page, .fitted, .automatic
}
Dismissal confirmation (iOS 26+):
swift
.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .dismissalConfirmationDialog("Discard changes?", shouldPresent: hasUnsavedChanges) {
            Button("Discard", role: .destructive) { discardChanges() }
        }
}
Enum-driven sheet routing: Centralize sheets with an enum and a helper modifier. See
references/sheets.md
and
references/app-wiring.md
for full patterns.
当状态对应选中的模型时,优先使用
.sheet(item:)
而非
.sheet(isPresented:)
。Sheet内部应该自行处理操作逻辑并调用
dismiss()
关闭。
swift
@State private var selectedItem: Item?

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
}
弹窗尺寸控制(iOS 18+): 使用
.presentationSizing
控制sheet的尺寸:
swift
.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .presentationSizing(.form)  // 可选值:.form, .page, .fitted, .automatic
}
关闭确认(iOS 26+):
swift
.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .dismissalConfirmationDialog("Discard changes?", shouldPresent: hasUnsavedChanges) {
            Button("Discard", role: .destructive) { discardChanges() }
        }
}
枚举驱动的sheet路由: 使用枚举和辅助修饰符统一管理sheet,完整模式请查看
references/sheets.md
references/app-wiring.md

Tab-Based Navigation

标签页导航

iOS 26 introduces an expanded Tab API with
Tab
,
TabSection
, roles, and customization:
swift
struct MainTabView: View {
    @State private var selectedTab: AppTab = .home

    var body: some View {
        TabView(selection: $selectedTab) {
            Tab("Home", systemImage: "house", value: .home) {
                NavigationStack { HomeView() }
            }
            Tab("Search", systemImage: "magnifyingglass", value: .search) {
                NavigationStack { SearchView() }
            }
            Tab("Profile", systemImage: "person", value: .profile) {
                NavigationStack { ProfileView() }
            }
            Tab(role: .search) {
                SearchView()
            }
        }
        .tabBarMinimizeBehavior(.onScrollDown) // iOS 26: auto-hide tab bar on scroll
    }
}
iOS 26 additions:
  • Tab(role: .search)
    replaces the tab bar with a search field
  • .tabBarMinimizeBehavior(_:)
    --
    .onScrollDown
    ,
    .onScrollUp
    ,
    .never
  • TabSection
    for sidebar grouping,
    .tabViewSidebarHeader/Footer/BottomBar
    ,
    TabViewBottomAccessoryPlacement
See
references/tabview.md
for full TabView patterns and
references/app-wiring.md
for root shell wiring.
iOS 26引入了扩展的Tab API,支持
Tab
TabSection
、角色配置和自定义功能:
swift
struct MainTabView: View {
    @State private var selectedTab: AppTab = .home

    var body: some View {
        TabView(selection: $selectedTab) {
            Tab("Home", systemImage: "house", value: .home) {
                NavigationStack { HomeView() }
            }
            Tab("Search", systemImage: "magnifyingglass", value: .search) {
                NavigationStack { SearchView() }
            }
            Tab("Profile", systemImage: "person", value: .profile) {
                NavigationStack { ProfileView() }
            }
            Tab(role: .search) {
                SearchView()
            }
        }
        .tabBarMinimizeBehavior(.onScrollDown) // iOS 26特性:滚动时自动隐藏标签栏
    }
}
iOS 26新增特性:
  • Tab(role: .search)
    将标签栏替换为搜索框
  • .tabBarMinimizeBehavior(_:)
    支持
    .onScrollDown
    .onScrollUp
    .never
    三种行为
  • 支持
    TabSection
    侧边栏分组、
    .tabViewSidebarHeader/Footer/BottomBar
    TabViewBottomAccessoryPlacement
    等配置
完整的TabView模式请查看
references/tabview.md
,根容器配置请查看
references/app-wiring.md

Environment

环境变量

Custom Environment Values

自定义环境变量

swift
private struct ThemeKey: EnvironmentKey {
    static let defaultValue: Theme = .default
}

extension EnvironmentValues {
    var theme: Theme {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

// Usage
.environment(\.theme, customTheme)
@Environment(\.theme) var theme
swift
private struct ThemeKey: EnvironmentKey {
    static let defaultValue: Theme = .default
}

extension EnvironmentValues {
    var theme: Theme {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

// 使用方式
.environment(\.theme, customTheme)
@Environment(\.theme) var theme

Common Built-in Environment Values

常用内置环境变量

swift
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.dynamicTypeSize) var dynamicTypeSize
@Environment(\.horizontalSizeClass) var sizeClass
@Environment(\.isSearching) var isSearching
@Environment(\.openURL) var openURL
@Environment(\.modelContext) var modelContext
swift
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.dynamicTypeSize) var dynamicTypeSize
@Environment(\.horizontalSizeClass) var sizeClass
@Environment(\.isSearching) var isSearching
@Environment(\.openURL) var openURL
@Environment(\.modelContext) var modelContext

Async Data Loading

异步数据加载

Always use
.task
-- it cancels automatically on view disappear:
swift
struct ItemListView: View {
    @State var store = ItemStore()

    var body: some View {
        List(store.items) { item in
            ItemRow(item: item)
        }
        .task { await store.load() }
        .refreshable { await store.refresh() }
    }
}
Use
.task(id:)
to re-run when a dependency changes:
swift
.task(id: searchText) {
    guard !searchText.isEmpty else { return }
    await search(query: searchText)
}
Never create manual
Task
in
onAppear
unless you need to store a reference for cancellation. Exception:
Task {}
is acceptable in synchronous action closures (e.g., Button actions) for immediate state updates before async work.
始终使用
.task
,它会在视图消失时自动取消任务:
swift
struct ItemListView: View {
    @State var store = ItemStore()

    var body: some View {
        List(store.items) { item in
            ItemRow(item: item)
        }
        .task { await store.load() }
        .refreshable { await store.refresh() }
    }
}
使用
.task(id:)
在依赖项变更时重新执行任务:
swift
.task(id: searchText) {
    guard !searchText.isEmpty else { return }
    await search(query: searchText)
}
不要在
onAppear
中手动创建
Task
,除非你需要存储引用用于手动取消。例外情况:同步的动作闭包(比如Button点击事件)中可以使用
Task {}
,用于在异步工作执行前更新即时状态。

iOS 26+ New APIs

iOS 26+ 新API

Scroll Edge Effects

滚动边缘效果

swift
ScrollView {
    content
}
.scrollEdgeEffectStyle(.soft, for: .top)  // iOS 26: fading edge effect
.backgroundExtensionEffect()              // iOS 26: mirror/blur at safe area edges
swift
ScrollView {
    content
}
.scrollEdgeEffectStyle(.soft, for: .top)  // iOS 26特性:顶部渐变边缘效果
.backgroundExtensionEffect()              // iOS 26特性:安全区域边缘的镜像/模糊效果

@Animatable Macro (iOS 26+)

@Animatable 宏(iOS 26+)

Synthesizes
AnimatableData
conformance automatically:
swift
@Animatable
struct PulseEffect: ViewModifier {
    var scale: Double
    // scale is automatically animatable -- no manual AnimatableData needed
}
自动生成
AnimatableData
协议实现:
swift
@Animatable
struct PulseEffect: ViewModifier {
    var scale: Double
    // scale自动支持动画,无需手动编写AnimatableData代码
}

TextEditor Enhancements (iOS 26+)

TextEditor 增强(iOS 26+)

TextEditor
now accepts
AttributedString
for rich text editing, with
FindContext
for in-editor find/replace.
TextEditor
现在支持
AttributedString
富文本编辑,以及通过
FindContext
实现编辑器内查找替换功能。

Performance Guidelines

性能指南

  • Lazy stacks/grids: Use
    LazyVStack
    ,
    LazyHStack
    ,
    LazyVGrid
    ,
    LazyHGrid
    for large collections. Regular stacks render all children immediately.
  • Stable IDs: All items in
    List
    /
    ForEach
    must conform to
    Identifiable
    with stable IDs. Never use array indices.
  • Avoid body recomputation: Move filtering and sorting to computed properties or the model, not inline in
    body
    .
  • Equatable views: For complex views that re-render unnecessarily, conform to
    Equatable
    .
  • 懒加载栈/网格: 大型列表使用
    LazyVStack
    LazyHStack
    LazyVGrid
    LazyHGrid
    ,普通栈会立即渲染所有子元素。
  • 稳定ID:
    List
    /
    ForEach
    中的所有项必须实现
    Identifiable
    协议并提供稳定ID,禁止使用数组索引。
  • 避免body重复计算: 将过滤、排序逻辑移到计算属性或模型中,不要直接写在
    body
    内联代码中。
  • Equatable视图: 对于不必要频繁重绘的复杂视图,让其遵循
    Equatable
    协议。

Component Reference

组件参考

See
references/components-index.md
for the full index of component-specific guides:
  • Layout: List, ScrollView, Grids, Split Views
  • Navigation: NavigationStack, TabView, Sheets, Deep Links
  • Input: Form, Controls, Focus, Searchable, Input Toolbar
  • Presentation: Overlay/Toasts, Loading/Placeholders, Matched Transitions
  • Platform: Top Bar, Title Menus, Menu Bar, macOS Settings
  • Media & Theming: Media, Theming, Haptics
  • Architecture: App Wiring, Lightweight Clients
Each component file includes intent, minimal usage pattern, pitfalls, and performance notes.
完整的组件指南索引请查看
references/components-index.md
  • 布局: List、ScrollView、Grids、Split Views
  • 导航: NavigationStack、TabView、Sheets、Deep Links
  • 输入: Form、Controls、Focus、Searchable、Input Toolbar
  • 展示: Overlay/Toasts、Loading/Placeholders、Matched Transitions
  • 平台适配: Top Bar、Title Menus、Menu Bar、macOS Settings
  • 媒体与主题: Media、Theming、Haptics
  • 架构: App Wiring、Lightweight Clients
每个组件文件都包含设计意图、最简使用示例、常见陷阱和性能注意事项。

HIG Alignment

HIG 对齐

Follow Apple Human Interface Guidelines for layout, typography, color, and accessibility. Key rules:
  • Use semantic colors (
    Color.primary
    ,
    .secondary
    ,
    Color(uiColor: .systemBackground)
    ) for automatic light/dark mode
  • Use system font styles (
    .title
    ,
    .headline
    ,
    .body
    ,
    .caption
    ) for Dynamic Type support
  • Use
    ContentUnavailableView
    for empty and error states
  • Support adaptive layouts via
    horizontalSizeClass
  • Provide VoiceOver labels (
    .accessibilityLabel
    ) and support Dynamic Type accessibility sizes by switching layout orientation
See
references/hig-patterns.md
for full HIG pattern examples.
布局、排版、颜色和无障碍设计遵循苹果人类界面指南,核心规则:
  • 使用语义颜色(
    Color.primary
    .secondary
    Color(uiColor: .systemBackground)
    )实现自动明暗模式适配
  • 使用系统字体样式(
    .title
    .headline
    .body
    .caption
    )支持动态类型
  • 空状态和错误状态使用
    ContentUnavailableView
  • 通过
    horizontalSizeClass
    支持自适应布局
  • 提供VoiceOver标签(
    .accessibilityLabel
    ),通过切换布局方向支持动态类型无障碍尺寸
完整的HIG模式示例请查看
references/hig-patterns.md

Common Mistakes

常见错误

  1. Using
    @ObservedObject
    to create objects -- use
    @StateObject
    (legacy) or
    @State
    (modern)
  2. Heavy computation in view
    body
    -- move to model or computed property
  3. Not using
    .task
    for async work -- manual
    Task
    in
    onAppear
    leaks if not cancelled
  4. Array indices as
    ForEach
    IDs -- causes incorrect diffing and UI bugs
  5. Forgetting
    @Bindable
    --
    $property
    syntax on
    @Observable
    requires
    @Bindable
  6. Over-using
    @State
    -- only for view-local state; shared state belongs in
    @Observable
  7. Not extracting subviews -- long body blocks are hard to read and optimize
  8. Using
    NavigationView
    -- deprecated; use
    NavigationStack
  9. Inline closures in body -- extract complex closures to methods
  10. .sheet(isPresented:)
    when state represents a model -- use
    .sheet(item:)
    instead
  1. 使用
    @ObservedObject
    创建对象:应该使用
    @StateObject
    (遗留方案)或
    @State
    (现代方案)
  2. 在视图
    body
    中执行 heavy 计算:应该移到模型或计算属性中
  3. 异步工作不使用
    .task
    onAppear
    中手动创建的
    Task
    如果没有取消会造成泄露
  4. 使用数组索引作为
    ForEach
    的ID:会导致diff计算错误和UI异常
  5. 忘记添加
    @Bindable
    @Observable
    对象使用
    $property
    语法需要搭配
    @Bindable
  6. 过度使用
    @State
    :仅用于视图本地状态,共享状态应该放在
    @Observable
    对象中
  7. 不提取子视图:过长的body代码可读性差,也难以优化
  8. 使用
    NavigationView
    :已废弃,应该使用
    NavigationStack
  9. body中使用内联闭包:将复杂闭包提取为方法
  10. 状态对应模型时使用
    .sheet(isPresented:)
    :应该优先使用
    .sheet(item:)

Review Checklist

代码评审检查清单

  • @Observable
    used for shared state models (not
    ObservableObject
    on iOS 17+)
  • @State
    owns objects;
    let
    /
    @Bindable
    receives them
  • NavigationStack
    used (not
    NavigationView
    )
  • .task
    modifier for async data loading
  • LazyVStack
    /
    LazyHStack
    for large collections
  • Stable
    Identifiable
    IDs (not array indices)
  • Views decomposed into focused subviews
  • No heavy computation in view
    body
  • Environment used for deeply shared state
  • Custom
    ViewModifier
    for repeated styling
  • .sheet(item:)
    preferred over
    .sheet(isPresented:)
  • Sheets own their actions and call
    dismiss()
    internally
  • MV pattern followed -- no unnecessary view models
  • @Observable
    view model classes are
    @MainActor
    -isolated
  • Model types passed across concurrency boundaries are
    Sendable
  • iOS 17+版本使用
    @Observable
    作为共享状态模型(而非
    ObservableObject
  • @State
    持有对象,
    let
    /
    @Bindable
    接收对象
  • 使用
    NavigationStack
    (而非已废弃的
    NavigationView
  • 异步数据加载使用
    .task
    修饰符
  • 大型集合使用
    LazyVStack
    /
    LazyHStack
  • 使用稳定的
    Identifiable
    ID(而非数组索引)
  • 视图已拆分为职责单一的子视图
  • 视图
    body
    中无heavy计算逻辑
  • 深层共享状态使用环境变量传递
  • 重复样式封装为自定义
    ViewModifier
  • 优先使用
    .sheet(item:)
    而非
    .sheet(isPresented:)
  • Sheet内部自行处理操作并调用
    dismiss()
    关闭
  • 遵循MV模式,无不必要的视图模型
  • @Observable
    视图模型类添加了
    @MainActor
    隔离
  • 跨并发边界传递的模型类型遵循
    Sendable
    协议