使用現代 MV 架構、狀態管理、組合、隔離預覽與遷移指引,建構與審查 SwiftUI 畫面。涵蓋 @Observable 所有權、@State/@Bindable/@Environment 接線、畫面分解、ViewModifier、環境值、.task 載入、iOS 26+ 交接、Writing Tools、剪貼簿可用性與效能。適用於結構化 SwiftUI 狀態、管理 @Observable、組合畫面、預覽有意義的 UI 狀態,或修正 SwiftUI 模式。
SwiftUI Patterns
針對 iOS 26+ 與 Swift 6.3 的現代 SwiftUI 模式。涵蓋架構、狀態管理、畫面組合、環境接線、非同步載入、設計打磨與平台/分享整合。導覽、佈局、動畫與 Liquid Glass 模式位於專屬的兄弟技能中。除非特別標註,否則模式向下相容至 iOS 17。
目錄
- 架構:Model-View (MV) 模式
- 工作流程
- 狀態管理
- 畫面成員排序慣例
- 畫面組合
- 環境
- 非同步資料載入
- iOS 26+ 新 API
- 效能指南
- HIG 對齊
- Writing Tools (iOS 18+)
- 常見錯誤
- 審查清單
- 參考資料
範圍邊界: 本技能涵蓋架構、狀態所有權、組合、環境接線、非同步載入及相關的 SwiftUI 應用程式結構模式。詳細的導覽模式(包括 NavigationStack、NavigationSplitView、sheet、分頁與深度連結模式)由 swiftui-navigation 技能負責。詳細的佈局、容器與元件模式(包括 stacks、grids、lists、scroll view 模式、forms、controls、使用 .searchable 的搜尋 UI、overlays 及相關佈局元件)由 swiftui-layout-components 技能負責。詳細的動畫編排由 swiftui-animation 技能負責。Liquid Glass 採用、自訂玻璃控制項、scroll edge effects、.scrollEdgeEffectStyle 與 .backgroundExtensionEffect 由 swiftui-liquid-glass 技能負責。
工作流程
- 記錄目前的狀態所有權、動作、副作用、導覽與生命週期行為。
- 選擇能維持該合約的最小 MV/狀態/組合變更。
- 每個結構步驟後建置;在繼續前修正編譯器與隔離錯誤。
- 針對已載入、載入中、空白與錯誤狀態(視情況)呈現確定性預覽,包含所需的環境依賴。
- 演練重要的互動與副作用。若行為改變,還原測試環境,修正最小的邊界,然後重新執行相同的建置、預覽與互動檢查。
載入 行為保留的畫面重構 以重構現有畫面,以及 隔離預覽建構 以了解測試環境與依賴模式。
架構:Model-View (MV) 模式
預設採用 MV——畫面是輕量的狀態表達;模型與服務擁有商業邏輯。除非現有程式碼已使用 ViewModel,否則不要引入。
核心原則:
- 偏好使用
@State、@Environment、@Query、.task與.onChange進行編排 - 透過
@Environment注入服務與共享模型;保持畫面小巧且可組合 - 將大型畫面拆分為較小的子畫面,而非引入 ViewModel
- 測試模型、服務與商業邏輯;保持畫面簡單且宣告式
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("錯誤", 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/architecture-patterns.md。
狀態管理
@Observable 所有權規則
重要: 當 SwiftUI 畫面擁有、修改或綁定 @Observable 儲存體與 ViewModel 的屬性時,請將其隔離在 @MainActor 上。Observation 會追蹤變更,但不會讓共享的可變狀態變成執行緒安全。不接觸 UI 狀態的領域模型可以使用自己的隔離策略。
| 包裝器 | 使用時機 |
|---|---|
@State |
畫面擁有該物件或值。建立並管理生命週期。 |
let |
畫面接收一個 @Observable 物件。唯讀觀察——不需要包裝器。 |
@Bindable |
畫面接收一個 @Observable 物件,且需要雙向綁定($property)。 |
@Environment(Type.self) |
從環境存取共享的 @Observable 物件。 |
@State(值類型) |
畫面本機的簡單狀態:開關、計數器、文字欄位值。永遠設為 private。 |
@Binding |
與父畫面的 @State 或 @Bindable 屬性進行雙向連接。 |
所有權模式
// 綁定 UI 的 @Observable 儲存體——主執行緒隔離
@MainActor
@Observable final class ItemStore {
var title = ""
var items: [Item] = []
}
// 擁有該模型的畫面
struct ParentView: View {
@State private var viewModel = ItemStore()
var body: some View {
ChildView(store: viewModel)
.environment(viewModel)
}
}
// 讀取該模型的畫面(@Observable 不需要包裝器)
struct ChildView: View {
let store: ItemStore
var body: some View { Text(store.title) }
}
// 需要雙向存取的畫面
struct EditView: View {
@Bindable var store: ItemStore
var body: some View {
TextField("標題", text: $store.title)
}
}
// 從環境讀取的畫面
struct DeepView: View {
@Environment(ItemStore.self) private var store
var body: some View {
@Bindable var s = store
TextField("標題", text: $s.title)
}
}
細粒度追蹤: SwiftUI 只會重新渲染讀取到已變更屬性的畫面。如果畫面讀取 items 但未讀取 isLoading,則變更 isLoading 不會觸發重新渲染。這是相較於 ObservableObject 的一大效能優勢。
舊版 ObservableObject
僅在支援 iOS 16 或更早版本時使用。@StateObject → @State,@ObservedObject → let,@EnvironmentObject → @Environment(Type.self)。
畫面成員排序慣例
由上到下排序成員:1) @Environment 2) let 屬性 3) @State / 儲存屬性 4) 計算屬性 var 5) init 6) body 7) 畫面建構器 / 輔助方法 8) 非同步函式
畫面組合
提取子畫面
將畫面拆分為專注的子畫面。每個子畫面應有單一職責。
重構現有畫面時,請載入 行為保留的畫面重構
以了解動作/副作用邊界與建置/預覽驗證。
var body: some View {
VStack {
HeaderSection(title: title, isPinned: isPinned)
DetailsSection(details: details)
ActionsSection(onSave: onSave, onCancel: onCancel)
}
}
計算畫面屬性
對於小型、無狀態的片段,保留計算屬性的 some View。當片段出現以下任何訊號時,將其提取為專屬的 View 類型:
- 有意義的分支或大量佈局
- 擁有自己的狀態或非同步生命週期
- 比父畫面更窄的 Observation 依賴
- 有用的獨立預覽
- 複雜度足以遮蔽父畫面的資料流
縮小依賴範圍時,只傳遞子畫面需要的值、綁定與動作。如果這些形成一個龐大但內聚的介面,則傳遞功能範圍的 @Observable 模型。Observation 會將失效範圍限制在子畫面讀取的屬性,但應用程式範圍的儲存體仍會建立一個寬廣的介面;請保留給真正需要該內聚狀態的子畫面。
重複使用是分解的有用結果,而非必要條件。
Extensions 與 // MARK: - 用於組織大型檔案;它們不會建立畫面邊界或取代提取。
ViewBuilder 函式
用於不值得建立獨立結構的條件邏輯:
@ViewBuilder
private func statusBadge(for status: Status) -> some View {
switch status {
case .active: Text("啟用").foregroundStyle(.green)
case .inactive: Text("停用").foregroundStyle(.secondary)
}
}
自訂 View Modifier
將重複的樣式提取為 ViewModifier:
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.background)
.clipShape(.rect(cornerRadius: 12))
.shadow(radius: 2)
}
}
extension View { func cardStyle() -> some View { modifier(CardStyle()) } }
穩定的畫面樹
避免頂層的條件式畫面切換。偏好使用單一穩定的基礎畫面,並在區段或修飾詞內部加入條件。
當提取的畫面需要獨立的狀態覆蓋、確定性測試環境或環境設定時,請載入 隔離預覽建構。
環境
自訂環境值
使用 @Entry 建立自訂環境值與動作。它會為 EnvironmentValues 產生樣板程式碼。
extension EnvironmentValues {
@Entry var theme: Theme = .default
@Entry var refreshFeed: @Sendable () async -> Void = {}
}
// 使用方式
.environment(\.theme, customTheme)
.environment(\.refreshFeed) { await feedStore.refresh() }
@Environment(\.theme) private var theme
@Environment(\.refreshFeed) private var refreshFeed
對於相容 iOS 17 的程式碼或舊版相容性墊片,請改用手動的 EnvironmentKey 類型。
常見的內建環境值
@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
非同步資料載入
一律使用 .task——它會在畫面消失時自動取消:
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:) 在依賴變更時重新執行:
.task(id: searchText) {
guard !searchText.isEmpty else { return }
await search(query: searchText)
}
除非需要儲存參考以供取消,否則切勿在 onAppear 中手動建立 Task。例外情況:在同步動作閉包(例如 Button 動作)中,Task {} 可用於在非同步工作前立即更新狀態。
使用 swift-concurrency 處理取消處理器、debounce 與時鐘、AsyncSequence 或 actor 隔離。
iOS 26+ 新 API
將 .scrollEdgeEffectStyle、.backgroundExtensionEffect 與玻璃控制項導向 swiftui-liquid-glass;將 @Animatable 導向 swiftui-animation。TextEditor(text: Binding<AttributedString>) 是 iOS 26 的富文字編輯路徑。請在採用這些 API 的程式碼旁保留可用性檢查。
剪貼簿指令修飾詞並非 iOS 26 的預設值:.copyable、.cuttable 與基於指令的 .pasteDestination(for:action:validator:) 在目前的 Apple 文件中是 macOS 13+ 與 iOS/iPadOS/Mac Catalyst 27 beta。對於 iOS 26 目標,請使用 UIPasteboard 處理自訂剪貼簿指令,或使用拖放與 ShareLink 處理 Transferable 流程。請參閱 references/platform-and-sharing.md。
效能指南
- Lazy stacks/grids: 對於大型集合,使用
LazyVStack、LazyHStack、LazyVGrid、LazyHGrid。一般 stacks 會立即渲染所有子項目。 - 穩定的 ID:
List/ForEach中的所有項目必須符合Identifiable並使用穩定的 ID。絕不要使用陣列索引。 - 避免 body 重複計算: 將過濾與排序移至計算屬性或模型,而非內嵌在
body中。 - Equatable 畫面: 對於不必要地重新渲染的複雜畫面,使其符合
Equatable。
HIG 對齊
遵循 Apple 人機介面指南的佈局、排版、色彩與無障礙設計。關鍵規則:
- 使用語意色彩(
Color.primary、.secondary、Color(uiColor: .systemBackground))以自動支援淺色/深色模式 - 使用系統字型樣式(
.title、.headline、.body、.caption)以支援 Dynamic Type - 使用
ContentUnavailableView處理空白與錯誤狀態 - 除非需要特定值,否則省略 stack 的
spacing:——nil(預設值)會使用平台適配的自適應間距 - 透過
horizontalSizeClass支援自適應佈局 - 提供 VoiceOver 標籤(
.accessibilityLabel),並透過切換佈局方向來支援 Dynamic Type 無障礙尺寸
請參閱 references/design-polish.md 了解 HIG、主題、觸覺回饋、焦點、轉場與載入模式。
Writing Tools (iOS 18+)
使用 .writingToolsBehavior(_:) 控制文字畫面上的 Apple Intelligence Writing Tools 體驗。
| 層級 | 效果 | 使用時機 |
|---|---|---|
.complete |
完整的內嵌改寫(校對、改寫、轉換) | 筆記、電子郵件、文件 |
.limited |
簡化的覆蓋面板體驗 | 程式碼編輯器、驗證表單 |
.disabled |
完全隱藏 Writing Tools | 密碼、搜尋列 |
.automatic |
系統根據上下文選擇(預設) | 大多數畫面 |
TextEditor(text: $body)
.writingToolsBehavior(.complete)
TextField("搜尋…", text: $query)
.writingToolsBehavior(.disabled)
偵測活躍工作階段: 讀取 UITextView(UIKit)上的 isWritingToolsActive,以延遲驗證或暫停復原分組,直到改寫完成。
常見錯誤
-
使用
@ObservedObject建立物件——請使用@StateObject(舊版)或@State(現代) -
在畫面
body中進行大量計算——移至模型或計算屬性 -
未使用
.task處理非同步工作——在onAppear中手動建立Task若未取消會造成洩漏 -
使用陣列索引作為
ForEach的 ID——導致錯誤的差異比對與 UI 錯誤 -
忘記
@Bindable——@Observable上的$property語法需要@Bindable -
過度使用
@State——僅用於畫面本機狀態;共享狀態應屬於@Observable -
將複雜或可獨立預覽的區段保留為計算屬性——請提取為
View類型;extensions 與// MARK:僅用於組織 -
使用
NavigationView——已棄用;請使用NavigationStack -
在
foregroundStyle(_:)更符合語意樣式時使用foregroundColor(_:) -
在 body 中使用內嵌閉包——將複雜閉包提取為方法
-
當狀態代表模型時使用
.sheet(isPresented:)——請改用.sheet(item:) -
在例行分支中使用
AnyView——型別抹消會隱藏結構,可能損害效能或身份敏感的轉場。請使用@ViewBuilder、Group或泛型,除非 API 確實需要異質畫面儲存。請參閱 references/deprecated-migration.md -
將
@AppStorage放在@Observable類別中。@AppStorage是畫面的DynamicProperty;請將其保留在View中,或在模型中公開一個由UserDefaults支援的一般觀察屬性。 -
在每個 stack 上硬編碼
spacing:——省略它以獲得自適應平台間距;僅在值有特定意圖時指定 -
將
.copyable、.cuttable或基於指令的.pasteDestination(for:action:validator:)視為 iOS 16/iOS 26 API——它們在目前的 Apple 文件中是 macOS 13+ 與 iOS/iPadOS/Mac Catalyst 27 beta。對於 iOS 26 目標,請使用UIPasteboard、拖放或ShareLink。 -
將現代預設值視為正式棄用——
#Preview是現代預覽預設值,但PreviewProvider是舊版而非編譯器棄用。EditButton、.onDelete與.onMove在編輯模式列表工作流程中仍然有效;請使用.swipeActions處理上下文列動作。 -
為了停止預覽崩潰而將必要的依賴設為可選——請改為安裝確定性預覽依賴,避免使用即時網路、認證、生產資料庫或全域單例
審查清單
- [ ] 共享狀態模型使用
@Observable(iOS 17+ 不使用ObservableObject) - [ ]
@State擁有物件;let/@Bindable接收它們 - [ ] 已檢查遷移與可用性聲明是否符合當前平台支援,特別是剪貼簿與分享 API
- [ ] 使用
NavigationStack(非NavigationView) - [ ] 使用
.task修飾詞處理非同步資料載入 - [ ] 大型集合使用
LazyVStack/LazyHStack - [ ] 使用穩定的
IdentifiableID(非陣列索引) - [ ] 提取使用分支/佈局、生命週期、依賴、預覽或父流程訊號;小型無狀態片段保留為計算屬性
- [ ] Extensions 與
// MARK:僅用於組織檔案 - [ ] 僅結構重構保留行為;使用精簡的動作/生命週期方法,將可重複使用的邏輯保留在服務/模型中,然後建置並呈現有用的預覽
- [ ] 預覽涵蓋有意義的已載入/載入中/空白/錯誤狀態,使用確定性測試環境與每個必要的環境依賴
- [ ] 畫面
body中無大量計算 - [ ] 使用環境處理深度共享的狀態
- [ ] 當語意樣式優於固定色彩時,使用
foregroundStyle(_:) - [ ] 重複樣式使用自訂
ViewModifier - [ ] 偏好使用
.sheet(item:)而非.sheet(isPresented:) - [ ] Sheet 擁有自己的動作並在內部呼叫
dismiss() - [ ] 遵循 MV 模式——無不必要的 ViewModel
- [ ] 綁定 UI 的
@Observable儲存體與 ViewModel 已隔離在@MainActor上 - [ ] 跨並發邊界傳遞的模型類型為
Sendable - [ ] 除非需要特定值,否則省略 stack 的
spacing:(偏好自適應預設值)
參考資料
- 架構、應用程式接線與輕量客戶端:references/architecture-patterns.md
- 設計打磨(HIG、主題、觸覺回饋、轉場、載入、焦點):references/design-polish.md
- 已棄用 API 遷移:references/deprecated-migration.md
- 平台與分享模式(Transferable、剪貼簿可用性、媒體、選單、macOS 設定):references/platform-and-sharing.md
- 隔離預覽建構(狀態覆蓋、測試環境與環境依賴):references/preview-isolation.md
- 現有畫面重構(行為合約、動作/副作用邊界與驗證):references/view-refactoring.md






