本章深入讲解 SwiftUI 的布局机制(约束协商系统)、所有常用内置组件的完整用法,包括 Stack、Grid、GeometryReader、内置控件、Form 表单、List 列表、Sheet 弹窗等。
3.1 布局约束协商机制
SwiftUI 的布局遵循父视图提出建议尺寸,子视图决定自身大小的协商机制:
父视图 → 提出建议尺寸(proposal)→ 子视图
子视图 → 返回自身实际尺寸(size)→ 父视图
父视图 → 根据子视图尺寸定位
swift
struct LayoutMechanismDemo: View {
var body: some View {
// Text 根据内容决定尺寸,不接受父视图全部空间
Text("我决定自己的尺寸")
.background(.yellow) // 背景仅覆盖文字区域
// .frame 强制指定尺寸
Text("固定尺寸")
.frame(width: 200, height: 60)
.background(.blue.opacity(0.2))
// .frame(maxWidth: .infinity) 占满可用宽度
Text("撑满宽度")
.frame(maxWidth: .infinity)
.background(.green.opacity(0.2))
// frame 的 alignment 参数
Text("左对齐")
.frame(maxWidth: .infinity, alignment: .leading)
.background(.orange.opacity(0.2))
}
}
3.2 Stack 布局容器
swift
// VStack:垂直排列
VStack(alignment: .leading, spacing: 12) {
Text("标题").font(.headline)
Text("副标题").foregroundStyle(.secondary)
Button("操作") { }
}
// HStack:水平排列
HStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.font(.system(size: 44))
.foregroundStyle(.blue)
VStack(alignment: .leading) {
Text("用户名").font(.headline)
Text("user@example.com").font(.caption).foregroundStyle(.secondary)
}
Spacer() // 弹性占位,把后续内容推到末尾
Button(action: {}) {
Image(systemName: "chevron.right")
}
.foregroundStyle(.secondary)
}
.padding()
.background(.regularMaterial) // 磨砂玻璃效果
.cornerRadius(16)
// ZStack:叠加布局
ZStack(alignment: .bottomTrailing) {
// 底层:渐变背景图
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
colors: [Color(hex: "#667eea"), Color(hex: "#764ba2")],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(height: 160)
// 中层:文字内容
VStack(alignment: .leading, spacing: 8) {
Text("高级会员").font(.headline).foregroundStyle(.white)
Text("享受全部功能").font(.subheadline).foregroundStyle(.white.opacity(0.8))
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
.padding(20)
// 顶层:角标
Label("推荐", systemImage: "star.fill")
.font(.caption.bold())
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(.orange)
.foregroundStyle(.white)
.cornerRadius(10)
.padding(12)
}
3.3 Grid 网格布局
swift
struct GridDemo: View {
// 自适应列(最小宽度 100pt,列数自动计算)
let adaptiveColumns = [GridItem(.adaptive(minimum: 100))]
// 固定 3 列
let fixedColumns = Array(repeating: GridItem(.flexible()), count: 3)
// 不等宽列
let unequalColumns = [
GridItem(.fixed(80)), // 固定 80pt
GridItem(.flexible()), // 剩余空间
GridItem(.fixed(60)), // 固定 60pt
]
var body: some View {
ScrollView {
// LazyVGrid:垂直网格(懒加载)
LazyVGrid(columns: adaptiveColumns, spacing: 16) {
ForEach(0..<20) { index in
ProductCell(index: index)
}
}
.padding()
// Grid(iOS 16+):精准对齐的静态网格
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 10) {
GridRow {
Text("姓名").foregroundStyle(.secondary)
Text("张三")
}
GridRow {
Text("邮箱").foregroundStyle(.secondary)
Text("zhangsan@example.com")
}
GridRow {
Text("职位").foregroundStyle(.secondary)
Text("iOS 开发工程师")
}
}
.padding()
.background(.regularMaterial)
.cornerRadius(12)
}
}
}
3.4 GeometryReader - 动态布局
swift
struct GeometryDemo: View {
var body: some View {
// GeometryReader 获取父视图的实际尺寸
GeometryReader { geometry in
let width = geometry.size.width
let height = geometry.size.height
VStack(spacing: 0) {
// 上半部分占 60%
Rectangle()
.fill(.blue.gradient)
.frame(height: height * 0.6)
.overlay {
Text("宽度:\(Int(width))pt")
.foregroundStyle(.white)
}
// 下半部分占 40%
Rectangle()
.fill(.orange.gradient)
.frame(height: height * 0.4)
}
}
.ignoresSafeArea()
// 使用 containerRelativeFrame(iOS 17+,推荐)
ScrollView(.horizontal) {
HStack {
ForEach(0..<5) { index in
RoundedRectangle(cornerRadius: 20)
.fill(Color.systemColor(index: index))
// 每个卡片占屏幕宽度的 80%
.containerRelativeFrame(.horizontal, count: 1,
span: 1, spacing: 16) { length, _ in
length * 0.8
}
.frame(height: 200)
}
}
}
.scrollTargetBehavior(.viewAligned) // iOS 17 分页滚动
}
}
3.5 常用内置组件
swift
struct BuiltinComponentsDemo: View {
@State private var sliderValue = 0.5
@State private var stepperCount = 0
@State private var isOn = true
@State private var selectedDate = Date()
@State private var selectedColor = Color.blue
var body: some View {
Form {
Section("交互控件") {
// Slider
VStack(alignment: .leading) {
Text("音量:\(Int(sliderValue * 100))%")
Slider(value: $sliderValue, in: 0...1, step: 0.01) {
Text("音量")
} minimumValueLabel: {
Image(systemName: "speaker")
} maximumValueLabel: {
Image(systemName: "speaker.3")
}
.tint(.blue)
}
// Stepper
Stepper("数量:\(stepperCount)", value: $stepperCount, in: 0...99)
// Toggle
Toggle("开启通知", isOn: $isOn)
Toggle("自动更新", isOn: $isOn)
.toggleStyle(.button) // 按钮样式
// Picker
Picker("主题", selection: $selectedColor) {
Text("蓝色").tag(Color.blue)
Text("绿色").tag(Color.green)
Text("红色").tag(Color.red)
}
.pickerStyle(.segmented)
}
Section("日期与时间") {
DatePicker("选择日期",
selection: $selectedDate,
in: Date()...,
displayedComponents: [.date])
DatePicker("选择时间",
selection: $selectedDate,
displayedComponents: [.hourAndMinute])
.datePickerStyle(.wheel)
}
Section("颜色与进度") {
ColorPicker("选择颜色", selection: $selectedColor)
ProgressView("下载进度", value: 0.65)
.progressViewStyle(.linear)
.tint(.green)
ProgressView()
.progressViewStyle(.circular)
.controlSize(.large)
}
}
}
}
3.6 Form 表单系统
swift
// 完整注册表单
struct RegisterFormView: View {
@State private var username = ""
@State private var email = ""
@State private var password = ""
@State private var confirmPassword = ""
@State private var birthday = Date()
@State private var selectedRole = "普通用户"
@State private var agreeTerms = false
// 实时表单验证
var usernameError: String? {
guard !username.isEmpty else { return nil }
if username.count < 3 { return "用户名至少 3 个字符" }
let pattern = "^[a-zA-Z0-9_]+$"
if username.range(of: pattern, options: .regularExpression) == nil {
return "只能包含字母、数字和下划线"
}
return nil
}
var isFormValid: Bool {
!username.isEmpty && !email.isEmpty && !password.isEmpty
&& password == confirmPassword && email.contains("@")
&& agreeTerms && usernameError == nil
}
var body: some View {
NavigationStack {
Form {
Section("账号信息") {
TextField("用户名(字母/数字/下划线)", text: $username)
.textContentType(.username)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
if let error = usernameError {
Label(error, systemImage: "exclamationmark.circle")
.foregroundStyle(.red)
.font(.caption)
}
TextField("邮箱", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
SecureField("密码", text: $password)
.textContentType(.newPassword)
SecureField("确认密码", text: $confirmPassword)
if !password.isEmpty && !confirmPassword.isEmpty
&& password != confirmPassword {
Label("两次密码不一致", systemImage: "xmark.circle")
.foregroundStyle(.red)
.font(.caption)
}
}
Section("个人信息") {
DatePicker("生日",
selection: $birthday,
in: ...Date(),
displayedComponents: .date)
Picker("角色", selection: $selectedRole) {
ForEach(["普通用户", "开发者", "内容创作者"], id: \.self) {
Text($0).tag($0)
}
}
}
Section {
Toggle("同意《用户服务协议》和《隐私政策》",
isOn: $agreeTerms)
}
Section {
Button("注册") { submitForm() }
.frame(maxWidth: .infinity)
.disabled(!isFormValid)
}
}
.navigationTitle("创建账号")
}
}
func submitForm() { /* 提交逻辑 */ }
}
3.7 List 列表系统
swift
struct ListDemo: View {
@State private var items: [TaskItem] = TaskItem.samples
@State private var isLoading = false
@State private var searchText = ""
var filteredItems: [TaskItem] {
searchText.isEmpty ? items : items.filter {
$0.title.localizedCaseInsensitiveContains(searchText)
}
}
var body: some View {
NavigationStack {
List {
// 分组列表
Section("进行中") {
ForEach(filteredItems.filter { !$0.isDone }) { item in
NavigationLink(value: item) {
TaskRowView(item: item)
}
// 左滑辅助操作(蓝/黄色按钮)
.swipeActions(edge: .leading, allowsFullSwipe: false) {
Button { markImportant(item) } label: {
Label("重要", systemImage: "flag")
}.tint(.orange)
}
// 右滑主操作(默认红色删除)
.swipeActions(edge: .trailing) {
Button(role: .destructive) { delete(item) } label: {
Label("删除", systemImage: "trash")
}
}
}
.onMove { indices, offset in
items.move(fromOffsets: indices, toOffset: offset)
}
}
Section("已完成") {
ForEach(filteredItems.filter { $0.isDone }) { item in
TaskRowView(item: item)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
// 底部加载
if isLoading {
HStack {
Spacer()
ProgressView("加载更多...")
Spacer()
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $searchText, prompt: "搜索任务")
.refreshable { await refresh() } // 下拉刷新
.navigationTitle("任务列表")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
EditButton() // 排序/删除模式
}
}
.navigationDestination(for: TaskItem.self) { item in
TaskDetailView(item: item)
}
}
}
func delete(_ item: TaskItem) {
items.removeAll { $0.id == item.id }
}
func markImportant(_ item: TaskItem) { }
func refresh() async {
isLoading = true
try? await Task.sleep(for: .seconds(1.5))
isLoading = false
}
}
3.8 弹窗系统
swift
struct AlertSheetDemo: View {
@State private var showDeleteAlert = false
@State private var showAddSheet = false
@State private var showActionSheet = false
@State private var showContextMenu = false
@State private var toastMessage: String?
var body: some View {
VStack(spacing: 20) {
// ① Alert - 确认对话框
Button("删除项目") { showDeleteAlert = true }
.alert("确认删除", isPresented: $showDeleteAlert) {
Button("取消", role: .cancel) {}
Button("删除", role: .destructive) {
showToast("已删除")
}
} message: {
Text("删除后无法恢复,确定继续?")
}
// ② Sheet - 底部弹出页(支持拖拽和多段高度)
Button("添加内容") { showAddSheet = true }
.sheet(isPresented: $showAddSheet) {
AddContentView()
.presentationDetents([.height(300), .medium, .large])
.presentationDragIndicator(.visible)
.presentationCornerRadius(24)
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
}
// ③ ConfirmationDialog - 操作选单(原 ActionSheet)
Button("更多操作") { showActionSheet = true }
.confirmationDialog("选择操作", isPresented: $showActionSheet,
titleVisibility: .visible) {
Button("📷 拍照") { }
Button("🖼️ 从相册选择") { }
Button("📁 从文件选择") { }
Button("删除", role: .destructive) { }
Button("取消", role: .cancel) {}
}
// ④ Context Menu - 长按菜单
Image(systemName: "photo.fill")
.font(.system(size: 60))
.foregroundStyle(.blue)
.contentShape(Rectangle())
.contextMenu {
Button { } label: {
Label("分享", systemImage: "square.and.arrow.up")
}
Button { } label: {
Label("收藏", systemImage: "heart")
}
Divider()
Button(role: .destructive) { } label: {
Label("删除", systemImage: "trash")
}
} preview: {
// 长按预览内容
Image(systemName: "photo.fill")
.font(.system(size: 200))
.padding(40)
}
}
.padding()
// ⑤ Toast 提示(overlay 实现)
.overlay(alignment: .bottom) {
if let message = toastMessage {
Text(message)
.padding(.horizontal, 20)
.padding(.vertical, 12)
.background(.black.opacity(0.8))
.foregroundStyle(.white)
.cornerRadius(25)
.padding(.bottom, 40)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.animation(.spring(), value: toastMessage)
}
func showToast(_ message: String) {
toastMessage = message
Task {
try? await Task.sleep(for: .seconds(2))
toastMessage = nil
}
}
}
章节总结
| 布局/组件 | 核心 API | 适用场景 |
|---|---|---|
| VStack/HStack/ZStack | Stack 容器 | 大多数布局基础 |
| LazyVGrid/LazyHGrid | Grid | 网格、瀑布流 |
| GeometryReader | 动态尺寸布局 | 百分比布局、屏幕适配 |
| ScrollView | 滚动容器 | 超出屏幕的内容 |
| List | 高性能可交互列表 | 数据列表、设置页 |
| Form | 表单容器 | 注册、设置、编辑 |
| .sheet | 底部弹出页 | 轻量页面、选择器 |
| .alert | 确认对话框 | 危险操作确认 |
| .contextMenu | 长按菜单 | 内容操作 |
Demo 说明
| 文件 | 演示内容 |
|---|---|
StackLayoutDemo.swift |
VStack/HStack/ZStack/Spacer 布局 |
GridGeometryDemo.swift |
Grid 网格 + GeometryReader 动态布局 |
ComponentsShowcaseDemo.swift |
Slider/Stepper/Toggle/Picker/DatePicker |
FormValidationDemo.swift |
完整表单校验 |
ListInteractionDemo.swift |
下拉刷新/左滑操作/搜索/排序 |
SheetAlertDemo.swift |
Sheet/Alert/ConfirmationDialog/ContextMenu |
📎 扩展内容补充
来源:第三章_状态管理.md
本章概述:系统讲解 SwiftUI 状态管理体系,从内置的 @State/@Binding,到 @Observable、Combine,再到企业级的 TCA(The Composable Architecture)框架,并对应 Flutter 的状态管理方案。
Flutter vs iOS 状态管理对照
| Flutter | iOS SwiftUI | 适用场景 |
|---|---|---|
setState |
@State |
组件内部局部状态 |
InheritedWidget |
@Environment |
跨层级只读共享 |
ChangeNotifier + Provider |
@Observable(Swift 5.9) |
跨组件共享状态 |
Riverpod |
@Observable + @Environment |
现代状态管理 |
BLoC / Cubit |
TCA |
单向数据流 + 测试 |
ValueNotifier |
@Published + Combine |
响应式数据流 |
3.1 @State / @Binding / @Environment
概念讲解
@State - 局部状态
swift
// @State 是视图私有的状态,变化时触发 UI 重建
// 只能在当前 View 及其子视图中修改
// 等同于 Flutter 的 setState + 变量
struct CounterView: View {
@State private var count = 0 // 整数状态
@State private var text = "" // 字符串状态
@State private var isVisible = true // 布尔状态
var body: some View {
VStack(spacing: 16) {
Text("计数:\(count)")
.font(.largeTitle.bold())
HStack(spacing: 20) {
Button {
count -= 1
} label: {
Image(systemName: "minus.circle.fill")
.font(.system(size: 44))
.foregroundStyle(.red)
}
Button {
count += 1
} label: {
Image(systemName: "plus.circle.fill")
.font(.system(size: 44))
.foregroundStyle(.green)
}
}
TextField("输入文字", text: $text) // $text 是 Binding
.textFieldStyle(.roundedBorder)
if isVisible {
Text("我在这里:\(text)")
.transition(.opacity.combined(with: .slide))
}
Button("切换显示") {
withAnimation {
isVisible.toggle()
}
}
}
.padding()
}
}
@Binding - 双向数据绑定
swift
// @Binding 是父视图传给子视图的「引用」,子视图可以修改
// 等同于 Flutter 的 ValueChanged 回调 + 当前值的组合
struct ParentView: View {
@State private var isOn = false
@State private var selectedColor = Color.blue
var body: some View {
VStack {
// 把 $isOn 传给子视图(Binding)
CustomToggle(isOn: $isOn)
ColorPicker("选择颜色", selection: $selectedColor)
Text(isOn ? "已开启" : "已关闭")
.foregroundStyle(selectedColor)
}
}
}
// 子视图接收 Binding
struct CustomToggle: View {
@Binding var isOn: Bool // 接收父视图的绑定
var body: some View {
Button {
isOn.toggle() // 修改会反映到父视图的 @State
} label: {
RoundedRectangle(cornerRadius: 30)
.fill(isOn ? Color.green : Color.gray)
.frame(width: 80, height: 44)
.overlay {
Circle()
.fill(.white)
.frame(width: 36, height: 36)
.offset(x: isOn ? 18 : -18)
.animation(.spring(duration: 0.3), value: isOn)
}
}
}
}
@Environment - 环境值
swift
// @Environment 读取系统或自定义的环境值
// 等同于 Flutter 的 Theme.of(context) 或 BuildContext
struct EnvDemo: View {
@Environment(\.colorScheme) var colorScheme // 深浅色模式
@Environment(\.horizontalSizeClass) var sizeClass // 设备尺寸
@Environment(\.dismiss) var dismiss // 关闭当前视图
@Environment(\.locale) var locale // 当前语言
var body: some View {
VStack {
Text("当前模式:\(colorScheme == .dark ? "深色" : "浅色")")
Text("设备类型:\(sizeClass == .compact ? "手机" : "平板")")
Button("关闭") { dismiss() }
}
}
}
项目中的应用 :
@Environment(\.dismiss)在 Sheet 和 NavigationStack 中关闭视图,@Environment(\.colorScheme)实现深浅色适配。
3.2 @Observable - 现代状态管理(Swift 5.9+)
概念讲解
@Observable 是 Swift 5.9 引入的宏,大幅简化了跨组件状态共享,是 Riverpod / GetX 的 iOS 对应物。
swift
import Observation
// 用 @Observable 标记模型类(类似 Riverpod 的 Notifier)
@Observable
class UserViewModel {
var username = ""
var email = ""
var isLoggedIn = false
var profile: UserProfile?
var isLoading = false
var errorMessage: String?
// 异步加载用户信息
func loadProfile() async {
isLoading = true
errorMessage = nil
do {
profile = try await UserService.shared.fetchProfile()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func logout() {
isLoggedIn = false
profile = nil
username = ""
}
}
// 在 App 入口注入(类似 Riverpod 的 ProviderScope)
@main
struct iOSDemosApp: App {
@State private var userViewModel = UserViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environment(userViewModel) // 注入全局ViewModel
}
}
}
// 任意子视图读取(类似 ConsumerWidget)
struct ProfileView: View {
@Environment(UserViewModel.self) var userVM
var body: some View {
Group {
if userVM.isLoading {
ProgressView("加载中...")
} else if let profile = userVM.profile {
VStack(alignment: .leading, spacing: 12) {
Text(profile.name).font(.title.bold())
Text(profile.email).foregroundStyle(.secondary)
Button("退出登录", role: .destructive) {
userVM.logout()
}
}
} else {
Button("加载个人信息") {
Task { await userVM.loadProfile() }
}
}
}
.task {
if userVM.profile == nil {
await userVM.loadProfile()
}
}
}
}
// 同级视图也可以修改(等同于 ref.read(provider.notifier).method())
struct SettingsView: View {
@Environment(UserViewModel.self) var userVM
var body: some View {
Form {
TextField("用户名", text: Bindable(userVM).username)
}
}
}
项目中的应用 :每个功能模块一个 ViewModel(如
CartViewModel、OrderViewModel),通过.environment()注入,子视图按需读取。
3.3 Combine 响应式编程
概念讲解
Combine 是苹果的响应式框架,等同于 Dart 的 Stream。
swift
import Combine
// 搜索防抖(类似 Flutter 中 StreamController + debounce)
@Observable
class SearchViewModel {
var searchText = ""
var results: [SearchResult] = []
var isSearching = false
private var cancellables = Set<AnyCancellable>()
init() {
setupSearch()
}
private func setupSearch() {
// 监听 searchText 变化,防抖 500ms
// 等同于 Flutter 的 debounceTime
$searchText // 注意:@Observable 中通过 Bindable($searchText)
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.removeDuplicates()
.filter { $0.count >= 2 }
.sink { [weak self] text in
Task { await self?.search(query: text) }
}
.store(in: &cancellables)
}
func search(query: String) async {
guard !query.isEmpty else { results = []; return }
isSearching = true
// 模拟网络搜索
try? await Task.sleep(for: .seconds(0.3))
results = SearchResult.mock(query: query)
isSearching = false
}
}
// Combine 操作符示例
class CombineExample {
func demonstrateOperators() {
// 1. map - 数据转换
let numbers = [1, 2, 3, 4, 5].publisher
numbers
.map { $0 * 2 }
.sink { print($0) } // 2, 4, 6, 8, 10
// 2. filter - 过滤
numbers
.filter { $0 % 2 == 0 }
.sink { print($0) } // 2, 4
// 3. combineLatest - 合并多个流
let usernamePublisher = PassthroughSubject<String, Never>()
let passwordPublisher = PassthroughSubject<String, Never>()
usernamePublisher
.combineLatest(passwordPublisher)
.map { username, password in
!username.isEmpty && password.count >= 6
}
.sink { isValid in
print("表单有效:\(isValid)")
}
// 4. flatMap - 处理异步操作
// 5. merge - 合并多个 Publisher
}
}
3.4 TCA - The Composable Architecture
概念讲解
TCA 是 Point-Free 出品的 iOS 架构框架,严格的单向数据流,类似 Flutter 的 BLoC/Cubit。
用户操作 → Action → Reducer(State, Action) → 新State → UI更新
↳ Effect(副作用)→ 异步操作 → Action
安装 TCA
在 Xcode → File → Add Package Dependencies 添加:
https://github.com/pointfreeco/swift-composable-architecture
TCA 核心代码
swift
import ComposableArchitecture
// 1. 定义 Feature(类比 BLoC)
@Reducer
struct CounterFeature {
// State - 视图状态(类比 BLoC State)
@ObservableState
struct State: Equatable {
var count = 0
var isLoading = false
var fact: String?
var errorMessage: String?
}
// Action - 用户行为/事件(类比 BLoC Event)
enum Action {
case incrementTapped
case decrementTapped
case resetTapped
case loadFactTapped
case factResponse(Result<String, Error>)
}
// 依赖注入(利于测试)
@Dependency(\.networkClient) var networkClient
// Reducer - 纯函数处理 Action(类比 BLoC mapEventToState)
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .incrementTapped:
state.count += 1
return .none // 无副作用
case .decrementTapped:
state.count -= 1
return .none
case .resetTapped:
state = State() // 重置状态
return .none
case .loadFactTapped:
state.isLoading = true
state.fact = nil
state.errorMessage = nil
return .run { [count = state.count] send in
// 异步副作用
let result = await Result {
try await networkClient.fetchNumberFact(count)
}
await send(.factResponse(result))
}
case .factResponse(.success(let fact)):
state.isLoading = false
state.fact = fact
return .none
case .factResponse(.failure(let error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
return .none
}
}
}
}
// 2. View 层(类比 BlocBuilder)
struct CounterView: View {
@Bindable var store: StoreOf<CounterFeature>
var body: some View {
VStack(spacing: 24) {
Text("\(store.count)")
.font(.system(size: 72, weight: .bold, design: .rounded))
HStack(spacing: 20) {
Button {
store.send(.decrementTapped)
} label: {
Image(systemName: "minus.circle.fill")
.font(.system(size: 48))
.foregroundStyle(.red)
}
Button {
store.send(.incrementTapped)
} label: {
Image(systemName: "plus.circle.fill")
.font(.system(size: 48))
.foregroundStyle(.green)
}
}
Button("获取数字趣闻") {
store.send(.loadFactTapped)
}
.buttonStyle(.borderedProminent)
if store.isLoading {
ProgressView("加载中...")
}
if let fact = store.fact {
Text(fact)
.multilineTextAlignment(.center)
.padding()
.background(.yellow.opacity(0.2))
.cornerRadius(12)
}
if let error = store.errorMessage {
Text(error).foregroundStyle(.red)
}
}
.padding()
}
}
// 3. 在父视图创建 Store(类比 BlocProvider)
struct ContentView: View {
var body: some View {
CounterView(
store: Store(initialState: CounterFeature.State()) {
CounterFeature()
._printChanges() // 调试:打印状态变化
}
)
}
}
TCA 组合(父子 Feature)
swift
// 购物车 Feature(复合)
@Reducer
struct ShoppingCartFeature {
@ObservableState
struct State: Equatable {
var counter = CounterFeature.State() // 子 Feature State
var items: [CartItem] = []
var totalPrice: Double = 0
}
enum Action {
case counter(CounterFeature.Action) // 子 Feature Action
case addItem(CartItem)
case removeItem(CartItem)
case checkout
}
var body: some ReducerOf<Self> {
Scope(state: \.counter, action: \.counter) {
CounterFeature() // 组合子 Reducer
}
Reduce { state, action in
switch action {
case .counter:
return .none // 已由子 Reducer 处理
case .addItem(let item):
state.items.append(item)
state.totalPrice += item.price
return .none
case .removeItem(let item):
state.items.removeAll { $0.id == item.id }
state.totalPrice -= item.price
return .none
case .checkout:
return .run { send in
// 结算逻辑
}
}
}
}
}
TCA 单元测试
swift
import ComposableArchitecture
import XCTest
final class CounterFeatureTests: XCTestCase {
// TCA 的测试极为简洁:给 Action → 验证 State 变化
@MainActor
func testIncrement() async {
let store = TestStore(
initialState: CounterFeature.State()
) {
CounterFeature()
}
await store.send(.incrementTapped) {
$0.count = 1 // 校验 state 变化
}
await store.send(.incrementTapped) {
$0.count = 2
}
}
@MainActor
func testLoadFact() async {
let store = TestStore(
initialState: CounterFeature.State()
) {
CounterFeature()
} withDependencies: {
$0.networkClient.fetchNumberFact = { _ in "42 是宇宙答案" }
}
await store.send(.loadFactTapped) {
$0.isLoading = true
}
await store.receive(.factResponse(.success("42 是宇宙答案"))) {
$0.isLoading = false
$0.fact = "42 是宇宙答案"
}
}
}
3.4 状态同步与生命周期
概念讲解
swift
struct LifecycleDemo: View {
@State private var data: [String] = []
var body: some View {
List(data, id: \.self) { Text($0) }
.onAppear {
// 视图出现时(类比 Flutter 的 initState)
print("视图出现")
}
.onDisappear {
// 视图消失时(类比 Flutter 的 dispose)
print("视图消失")
}
.task {
// 视图出现时自动执行异步任务,消失时自动取消
// 类比 Flutter 的 initState + dispose 组合
await loadData()
}
.task(id: searchQuery) {
// searchQuery 变化时重新执行
await search(query: searchQuery)
}
.onChange(of: selectedTab) { oldValue, newValue in
// 监听值变化(类比 Flutter 的 didUpdateWidget)
print("Tab 从 \(oldValue) 切换到 \(newValue)")
}
}
func loadData() async {
// 模拟异步数据加载
try? await Task.sleep(for: .seconds(1))
data = ["数据1", "数据2", "数据3"]
}
}
章节总结
| 状态管理方案 | 适用场景 | 复杂度 |
|---|---|---|
@State |
组件内局部UI状态 | ⭐ |
@Binding |
父子组件状态共享 | ⭐⭐ |
@Environment |
跨层级只读数据 | ⭐⭐ |
@Observable |
跨模块共享状态(推荐) | ⭐⭐⭐ |
Combine |
响应式数据流、搜索防抖 | ⭐⭐⭐⭐ |
TCA |
大型项目、需要严格测试 | ⭐⭐⭐⭐⭐ |
Demo 说明
本章对应 Demo 位于 iOS_demos/Chapter03/:
| Demo 文件 | 演示内容 |
|---|---|
StateBindingDemo.swift |
@State/@Binding 双向绑定演示 |
ObservableDemo.swift |
@Observable 跨组件状态管理 |
CombineDemo.swift |
Combine 响应式编程/搜索防抖 |
TCADemo.swift |
TCA 完整 Counter + 网络请求 |