第三章:布局与组件系统

本章深入讲解 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(如 CartViewModelOrderViewModel),通过 .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 + 网络请求
相关推荐
空中海2 小时前
第八章:iOS并发编程
macos·ios·cocoa
空中海4 小时前
第五章:i OS状态与数据流管理
ios
花间相见6 小时前
【大模型微调与部署01】—— ms-swift-3.12入门:安装、快速上手
开发语言·ios·swift
空中海6 小时前
第一章:Swift 语言核心
ios·cocoa·swift
90后的晨仔8 小时前
《SwiftUI 进阶第6章:列表与滚动视图》
ios
空中海8 小时前
第十章:iOS架构设计与工程化
macos·ios·cocoa
90后的晨仔15 小时前
《SwiftUI 进阶第7章:导航系统》
ios
90后的晨仔15 小时前
《swiftUI进阶 第9章SwiftUI 状态管理完全指南》
ios
90后的晨仔15 小时前
《 SwiftUI 进阶第8章:表单与设置界面》
ios