swiftui-layout-components

swiftui-layout-components

熱門

使用堆疊、網格、列表、滾動視圖、表單和控制器來建構 SwiftUI 佈局。涵蓋 VStack/HStack/ZStack、LazyVGrid/LazyHGrid、帶有區段和滑動操作的 List、支援 ScrollPosition 和滾動驅動顯示區域的 ScrollView、含驗證的 Form、Toggle/Picker/Slider、.searchable 以及疊加模式。適用於建構資料驅動佈局、集合視圖、分頁細節顯示、設定畫面、搜尋介面或暫時性疊加 UI。

931星標
0分支
更新於 2026/7/26
SKILL.md
readonlyread-only
name
swiftui-layout-components
description

使用堆疊、網格、列表、滾動視圖、表單和控制器來建構 SwiftUI 佈局。涵蓋 VStack/HStack/ZStack、LazyVGrid/LazyHGrid、帶有區段和滑動操作的 List、支援 ScrollPosition 和滾動驅動顯示區域的 ScrollView、含驗證的 Form、Toggle/Picker/Slider、.searchable 以及疊加模式。適用於建構資料驅動佈局、集合視圖、分頁細節顯示、設定畫面、搜尋介面或暫時性疊加 UI。

SwiftUI 佈局與元件

適用於 iOS 26+ 且使用 Swift 6.3 的 SwiftUI 應用程式之佈局與元件模式。涵蓋堆疊與網格佈局、列表模式、滾動視圖、表單、控制器、搜尋與疊加。除非另有說明,模式可向下相容至 iOS 17。

目錄

佈局基礎

標準堆疊

對於小型、固定大小的內容,使用 VStackHStackZStack。它們會立即渲染所有子視圖。

VStack(alignment: .leading) {
    Text(title).font(.headline)
    Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}

惰性堆疊

ScrollView 內部使用 LazyVStackLazyHStack 來處理大型或動態集合。它們會根據子視圖滾入視野時按需建立。

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
        }
    }
    .padding(.horizontal)
}

何時使用哪一種:

  • 非惰性堆疊: 小型、固定內容(標題、工具列、欄位較少的表單)
  • 惰性堆疊: 大型或未知大小的集合、動態消息、聊天訊息

網格佈局

對於圖示選擇器、媒體圖庫和密集的視覺選擇,使用 LazyVGrid。使用 .adaptive 欄位來建立可跨裝置尺寸縮放的佈局,或使用 .flexible 欄位來設定固定的欄位數量。

// 自適應網格 — 欄位會自動調整以適應
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]

LazyVGrid(columns: columns) {
    ForEach(items) { item in
        ThumbnailView(item: item)
            .aspectRatio(1, contentMode: .fit)
    }
}
// 固定 3 欄網格
let columns = Array(repeating: GridItem(.flexible(minimum: 100), spacing: 4), count: 3)

LazyVGrid(columns: columns, spacing: 4) {
    ForEach(items) { item in
        ThumbnailView(item: item)
    }
}

使用 .aspectRatio 來設定儲存格大小。切勿將 GeometryReader 放在惰性容器內部 — 它會強制立即測量並破壞惰性載入。如果需要讀取尺寸,請使用 .onGeometryChange(iOS 16+)。

完整的網格模式和設計選擇請參閱 references/grids.md

列表模式

對於動態消息風格的內容和設定列,使用 List,因為它內建了列重用、選取和輔助功能。

List {
    Section("一般") {
        NavigationLink("顯示器") { DisplaySettingsView() }
        NavigationLink("觸覺回饋") { HapticsSettingsView() }
    }
    Section("帳戶") {
        Button("登出", role: .destructive) { }
    }
}
.listStyle(.insetGrouped)

關鍵模式:

  • 動態消息佈局使用 .listStyle(.plain),設定畫面使用 .insetGrouped
  • 對於主題化表面,使用 .scrollContentBackground(.hidden) + 自訂背景
  • 使用 .listRowInsets(...).listRowSeparator(.hidden) 來控制間距和分隔線
  • 邊緣滾動: 使用 List + ScrollPosition 搭配 .scrollPosition($scrollPosition) 來執行頂部/底部滾動操作
  • 項目或區段跳轉: 使用 ScrollView + 惰性堆疊搭配 .scrollTargetLayout() 和穩定的目標,以實現可靠的跳轉到 ID 行為
  • 使用 .refreshable { } 實現下拉重新整理動態消息
  • 在應該可從頭到尾點擊的列上使用 .contentShape(Rectangle())
  • 對於佈局審查或遷移指南,以容器選擇和約束為主;保持程式碼片段簡短,並將彈簧、過渡和時機選擇延後到 swiftui-animation

iOS 26: 使用 .scrollEdgeEffectStyle(.soft, for: .top) 實現現代滾動邊緣效果。

完整的列表模式(包括帶有滾動到頂部的動態消息列表)請參閱 references/list.md

ScrollView

當你需要自訂佈局、混合內容或水平滾動時,使用 ScrollView 搭配惰性堆疊。

ScrollView(.horizontal, showsIndicators: false) {
    LazyHStack {
        ForEach(chips) { chip in
            ChipView(chip: chip)
        }
    }
}

ScrollPosition: 啟用宣告式、雙向的滾動位置追蹤和程式化滾動。

@State private var scrollPosition = ScrollPosition(edge: .bottom)

ScrollView {
    LazyVStack {
        ForEach(messages) { message in
            MessageRow(message: message)
        }
    }
    .scrollTargetLayout()
}
.scrollPosition($scrollPosition)
.onChange(of: messages.last?.id) {
    withAnimation { scrollPosition.scrollTo(edge: .bottom) }
}

safeAreaInset(edge:) 在不影響滾動佈局的情況下,將內容(輸入列、工具列)固定在鍵盤上方。

iOS 26 新增功能:

  • .scrollEdgeEffectStyle(.soft, for: .top) — 淡出邊緣效果
  • .backgroundExtensionEffect() — 在安全區域邊緣鏡像/模糊(謹慎使用,每個畫面一個)
  • .safeAreaBar(edge:) — 附加與滾動效果整合的欄位視圖

完整的 ScrollPosition、分頁顯示、縮放/裁切衝突和 iOS 26 邊緣效果模式,請參閱 references/scrollview.md

表單與控制器

Form

對於結構化的設定和輸入畫面,使用 Form。將相關控制器分組到 Section 區塊中。

Form {
    Section("通知") {
        Toggle("提及", isOn: $prefs.mentions)
        Toggle("追蹤", isOn: $prefs.follows)
    }
    Section("外觀") {
        Picker("主題", selection: $theme) {
            ForEach(Theme.allCases, id: \.self) { Text($0.title).tag($0) }
        }
        Slider(value: $fontScale, in: 0.5...1.5, step: 0.1)
    }
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)

使用 @FocusState 來管理輸入密集型表單中的鍵盤焦點。僅在獨立呈現或位於 sheet 中時,才將其包裝在 NavigationStack 中。

控制器

控制器 用途
Toggle 布林偏好設定
Picker 離散選擇;2-4 個選項使用 .segmented
Slider 數值範圍,附帶可見的數值標籤
DatePicker 日期/時間選擇
TextField 文字輸入,搭配 .keyboardType.textInputAutocapitalization

直接將控制器綁定到 @State@Binding@AppStorage。在 Form 區段中將相關控制器分組。使用 .disabled(...) 來反映鎖定或繼承的設定。當有助於清晰度時,在切換開關內部使用 Label 來結合圖示和文字。

對於大型選項集,避免使用 .pickerStyle(.segmented);請改用選單或內聯樣式。不要隱藏滑桿的標籤;始終顯示上下文。

完整的表單範例請參閱 references/form.md

可搜尋

使用 .searchable 新增原生搜尋 UI。使用 .searchScopes 實現多種模式,並使用 .task(id:) 實現防抖的非同步結果。

@MainActor
struct ExploreView: View {
  @State private var searchQuery = ""
  @State private var searchScope: SearchScope = .all
  @State private var isSearching = false
  @State private var results: [SearchResult] = []

  var body: some View {
    List {
      if isSearching {
        ProgressView()
      } else {
        ForEach(results) { result in
          SearchRow(result: result)
        }
      }
    }
    .searchable(
      text: $searchQuery,
      placement: .navigationBarDrawer(displayMode: .always),
      prompt: Text("搜尋")
    )
    .searchScopes($searchScope) {
      ForEach(SearchScope.allCases, id: \.self) { scope in
        Text(scope.title)
      }
    }
    .task(id: searchQuery) {
      await runSearch()
    }
  }

  private func runSearch() async {
    guard !searchQuery.isEmpty else {
      results = []
      return
    }
    isSearching = true
    defer { isSearching = false }
    try? await Task.sleep(for: .milliseconds(250))
    results = await fetchResults(query: searchQuery, scope: searchScope)
  }
}

當搜尋為空時顯示佔位符。對輸入進行防抖以避免過度擷取。將搜尋狀態保持在視圖本地。避免對空字串執行搜尋。

疊加與呈現

使用 .overlay(alignment:) 實現暫時性 UI(提示、橫幅),而不影響佈局。

struct AppRootView: View {
  @State private var toast: Toast?

  var body: some View {
    content
      .overlay(alignment: .top) {
        if let toast {
          ToastView(toast: toast)
            .transition(.move(edge: .top).combined(with: .opacity))
            .onAppear {
              Task {
                try? await Task.sleep(for: .seconds(2))
                withAnimation { self.toast = nil }
              }
            }
        }
      }
  }
}

對於暫時性 UI,優先使用疊加而非嵌入佈局堆疊。使用過場動畫和短暫的自動關閉計時器。將疊加對齊到清晰的邊緣(.top.bottom)。除非明確需要,否則避免使用會阻止所有互動的疊加。不要堆疊多個疊加;使用佇列或取代當前的提示。

對於模態路由、sheet 調整大小和全螢幕呈現策略,請移交給 swiftui-navigation 技能。

常見錯誤

  1. GeometryReader 放在惰性容器內部會破壞惰性載入;當需要尺寸時,請使用 .onGeometryChange
  2. 陣列索引會產生不穩定的 ForEach ID,並導致不正確的差異計算。
  3. 同軸巢狀滾動視圖會產生手勢衝突。
  4. 大型自訂或展開的 List 列應屬於 ScrollView + LazyVStack
  5. 大型選項集應使用選單或內聯選擇器樣式,而非 .segmented
  6. 除非有特定間距意圖,否則省略堆疊/網格的 spacing: 以使用平台自適應預設值。
  7. 滾動驅動的顯示應使用一個標準化的進度值,而非並行的布林值或重複的拖曳手勢。
  8. 將每幀的滾動幾何保持在本機,並避免變更用於計算進度的幾何。

審查清單

  • [ ] 對於大型或動態集合,使用 LazyVStack/LazyHStack
  • [ ] 所有 ForEach 項目使用穩定的 Identifiable ID(非陣列索引)
  • [ ] 惰性容器內部沒有 GeometryReader
  • [ ] List 樣式符合上下文(動態消息使用 .plain,設定使用 .insetGrouped
  • [ ] 對於結構化的輸入畫面使用 Form(而非自訂堆疊)
  • [ ] .searchable 使用 .task(id:) 對輸入進行防抖
  • [ ] 在資料來源支援下拉重新整理的地方加入 .refreshable
  • [ ] 疊加使用過場動畫和自動關閉計時器
  • [ ] 在可點擊的列上使用 .contentShape(Rectangle())
  • [ ] @FocusState 管理表單中的鍵盤焦點
  • [ ] 除非需要特定值,否則省略堆疊/網格的 spacing:
  • [ ] 滾動驅動的顯示使用一個標準化的進度值,並將幾何更新保持在狹窄的子樹中
  • [ ] 衝突的縮放/裁切互動會停用滾動,且離散的可見性效果不會驅動連續動畫

參考資料