第五章:i OS状态与数据流管理

本章系统讲解 SwiftUI 的状态管理体系:从单视图的 @State,到跨组件的 @Observable,到响应式编程 Combine,再到企业级架构 TCA(The Composable Architecture)。


5.1 @State - 局部状态

@State 是 SwiftUI 最基础的状态机制,用于管理当前 View 私有的可变状态。

swift 复制代码
struct CounterView: View {
    // @State 修饰的属性变化时,body 自动重新执行
    @State private var count = 0
    @State private var text = ""
    @State private var isVisible = true
    @State private var selectedColor = Color.blue
    
    var body: some View {
        VStack(spacing: 20) {
            // count 变化 → UI 自动更新
            Text("\(count)")
                .font(.system(size: 80, weight: .bold, design: .rounded))
            
            HStack(spacing: 24) {
                Button {
                    count -= 1
                } label: {
                    Image(systemName: "minus.circle.fill")
                        .font(.system(size: 48))
                        .foregroundStyle(.red)
                }
                
                Button {
                    count = 0  // 重置
                } label: {
                    Image(systemName: "arrow.counterclockwise.circle.fill")
                        .font(.system(size: 48))
                        .foregroundStyle(.orange)
                }
                
                Button {
                    count += 1
                } label: {
                    Image(systemName: "plus.circle.fill")
                        .font(.system(size: 48))
                        .foregroundStyle(.green)
                }
            }
            
            // 条件显示(带动画)
            if isVisible {
                TextField("输入文字", text: $text)
                    .textFieldStyle(.roundedBorder)
                    .transition(.scale.combined(with: .opacity))
            }
            
            Button("切换输入框") {
                withAnimation(.spring()) { isVisible.toggle() }
            }
        }
        .padding()
    }
}

@State 的工作原理

复制代码
SwiftUI 内部将 @State 值存储在 View 的外部(框架管理)
当 @State 值变化时:
  1. SwiftUI 标记当前 View 需要更新
  2. 下一个渲染循环中重新调用 body
  3. Diff 算法比较新旧 View 树,最小化更新

5.2 @Binding - 双向数据绑定

@Binding 让子视图可以读取并修改父视图的 @State,建立双向数据连接。

swift 复制代码
// 父视图:拥有状态
struct ParentView: View {
    @State private var selectedColor = Color.blue
    @State private var brightness = 0.5
    @State private var isRounded = true
    
    var body: some View {
        VStack {
            // 预览区:展示状态效果
            RoundedRectangle(cornerRadius: isRounded ? 20 : 0)
                .fill(selectedColor)
                .brightness(brightness - 0.5)
                .frame(height: 120)
                .padding()
            
            // 控制面板:传入 Binding
            ShapeControlPanel(
                selectedColor: $selectedColor,   // $ 前缀 = Binding
                brightness: $brightness,
                isRounded: $isRounded
            )
        }
    }
}

// 子视图:接收并修改 Binding
struct ShapeControlPanel: View {
    @Binding var selectedColor: Color   // 接收 Binding,可读可写
    @Binding var brightness: Double
    @Binding var isRounded: Bool
    
    var body: some View {
        Form {
            // 直接修改父视图的状态
            ColorPicker("颜色", selection: $selectedColor)
            
            VStack(alignment: .leading) {
                Text("亮度:\(Int(brightness * 100))%")
                Slider(value: $brightness)
            }
            
            Toggle("圆角", isOn: $isRounded)
            
            Button("重置") {
                // 通过 Binding 重置父视图状态
                selectedColor = .blue
                brightness = 0.5
                isRounded = true
            }
        }
    }
}

5.3 @Environment - 环境对象

@Environment 用于读取跨层级传递的环境数据,无需逐层手动传递。

swift 复制代码
// 系统环境值
@Environment(\.colorScheme) var colorScheme
@Environment(\.locale) var locale
@Environment(\.dismiss) var dismiss
@Environment(\.openURL) var openURL

// 自定义 ViewModel 注入到环境(iOS 17 @Observable)
@main
struct iOSDemosApp: App {
    @State private var userVM = UserViewModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(userVM)   // 注入到整个 App 的环境
        }
    }
}

// 任意子视图读取
struct DeepChildView: View {
    @Environment(UserViewModel.self) var userVM
    
    var body: some View {
        Text("用户:\(userVM.username)")
    }
}

5.4 @Observable - 现代跨组件状态管理

@Observable 是 Swift 5.9 引入的宏,简化且高效的跨组件状态管理方案(iOS 17+)。

swift 复制代码
import Observation

// 定义可观察的 ViewModel
@Observable
class ArticleViewModel {
    // 所有属性自动参与观察,无需 @Published
    var articles: [Article] = []
    var isLoading = false
    var errorMessage: String?
    var selectedCategory: Category = .all
    var searchText = ""
    
    // 计算属性也可以被观察
    var filteredArticles: [Article] {
        var result = articles
        if selectedCategory != .all {
            result = result.filter { $0.category == selectedCategory }
        }
        if !searchText.isEmpty {
            result = result.filter {
                $0.title.localizedCaseInsensitiveContains(searchText)
            }
        }
        return result
    }
    
    var hasError: Bool { errorMessage != nil }
    
    // 异步操作
    func loadArticles() async {
        isLoading = true
        errorMessage = nil
        
        do {
            articles = try await ArticleService.shared.fetchAll()
        } catch {
            errorMessage = error.localizedDescription
        }
        
        isLoading = false
    }
    
    func refreshIfNeeded() async {
        guard articles.isEmpty else { return }
        await loadArticles()
    }
    
    func delete(_ article: Article) {
        articles.removeAll { $0.id == article.id }
        Task { try? await ArticleService.shared.delete(article.id) }
    }
}

// 视图使用(精准更新:只有读取的属性变化时才重建)
struct ArticleListView: View {
    @Environment(ArticleViewModel.self) var vm
    
    var body: some View {
        Group {
            if vm.isLoading && vm.articles.isEmpty {
                ProgressView("加载中...")
            } else if vm.hasError {
                ErrorView(message: vm.errorMessage!) {
                    Task { await vm.loadArticles() }
                }
            } else {
                articleList
            }
        }
        .task { await vm.refreshIfNeeded() }
        .searchable(text: Bindable(vm).searchText)
    }
    
    var articleList: some View {
        List(vm.filteredArticles) { article in
            ArticleRow(article: article)
                .swipeActions {
                    Button(role: .destructive) { vm.delete(article) } label: {
                        Label("删除", systemImage: "trash")
                    }
                }
        }
    }
}

// Bindable(iOS 17):为 @Observable 创建 Binding
struct ArticleEditorView: View {
    @Bindable var vm: ArticleViewModel
    
    var body: some View {
        Form {
            // 直接对 @Observable 属性做 Binding
            TextField("搜索", text: $vm.searchText)
            Picker("分类", selection: $vm.selectedCategory) {
                ForEach(Category.allCases) { category in
                    Text(category.name).tag(category)
                }
            }
        }
    }
}

5.5 Combine - 响应式编程

Combine 是 Apple 的响应式框架,适合处理异步数据流事件序列

swift 复制代码
import Combine

// 搜索防抖(最常见的 Combine 应用场景)
@Observable
class SearchViewModel {
    var searchText = ""
    var results: [SearchResult] = []
    var isSearching = false
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 监听 searchText,防抖 500ms 后执行搜索
        // (@Observable 与 Combine 结合需要通过 KVO 或自定义方案)
    }
    
    func search(_ query: String) async {
        guard query.count >= 2 else { results = []; return }
        isSearching = true
        
        try? await Task.sleep(for: .milliseconds(300))
        guard !Task.isCancelled else { return }
        
        results = await SearchService.shared.search(query: query)
        isSearching = false
    }
}

// Combine 核心操作符演示
class CombineOperatorsDemo {
    var cancellables = Set<AnyCancellable>()
    
    func demonstrate() {
        // 1. Just:单值 Publisher
        Just(42)
            .map { $0 * 2 }
            .sink { print($0) }     // 84
            .store(in: &cancellables)
        
        // 2. 数组 Publisher
        [1, 2, 3, 4, 5].publisher
            .filter { $0.isMultiple(of: 2) }
            .map { "偶数: \($0)" }
            .sink { print($0) }     // 偶数: 2, 偶数: 4
            .store(in: &cancellables)
        
        // 3. PassthroughSubject(手动发送值)
        let subject = PassthroughSubject<String, Never>()
        subject
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .sink { print("搜索:\($0)") }
            .store(in: &cancellables)
        
        subject.send("S")
        subject.send("Sw")
        subject.send("Swi")       // 300ms 内只有最后一个触发
        subject.send("Swift")
        
        // 4. CombineLatest - 合并多个 Publisher
        let username = PassthroughSubject<String, Never>()
        let password = PassthroughSubject<String, Never>()
        
        username
            .combineLatest(password)
            .map { user, pwd in !user.isEmpty && pwd.count >= 6 }
            .removeDuplicates()
            .sink { isValid in print("表单有效:\(isValid)") }
            .store(in: &cancellables)
        
        // 5. flatMap - 将事件转为新 Publisher
        subject
            .flatMap { query -> AnyPublisher<[String], Never> in
                // 每次输入触发新的搜索请求
                return searchPublisher(for: query)
            }
            .sink { results in print("结果数量:\(results.count)") }
            .store(in: &cancellables)
    }
}

5.6 TCA - The Composable Architecture

TCA 是严格的单向数据流架构,适用于大型项目和高测试要求的场景。

swift 复制代码
import ComposableArchitecture

// ① 定义 Feature(Reducer)
@Reducer
struct ShoppingCartFeature {
    
    // State:所有视图状态(纯数据,Equatable)
    @ObservableState
    struct State: Equatable {
        var items: [CartItem] = []
        var isCheckingOut = false
        var orderResult: OrderResult?
        var errorMessage: String?
        
        var totalPrice: Double {
            items.reduce(0) { $0 + $1.price * Double($1.quantity) }
        }
        
        var isEmpty: Bool { items.isEmpty }
    }
    
    // Action:所有可能的用户行为和系统事件
    enum Action {
        case addItem(CartItem)
        case removeItem(id: CartItem.ID)
        case updateQuantity(id: CartItem.ID, quantity: Int)
        case clearCart
        case checkoutButtonTapped
        case checkoutResponse(Result<OrderResult, Error>)
        case dismissError
    }
    
    // 依赖(便于单元测试时 Mock)
    @Dependency(\.orderClient) var orderClient
    
    // Reducer:纯函数,处理 Action → 新 State
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .addItem(let item):
                if let index = state.items.firstIndex(where: { $0.id == item.id }) {
                    state.items[index].quantity += 1
                } else {
                    state.items.append(item)
                }
                return .none
                
            case .removeItem(let id):
                state.items.removeAll { $0.id == id }
                return .none
                
            case .updateQuantity(let id, let quantity):
                if quantity <= 0 {
                    state.items.removeAll { $0.id == id }
                } else if let index = state.items.firstIndex(where: { $0.id == id }) {
                    state.items[index].quantity = quantity
                }
                return .none
                
            case .clearCart:
                state.items = []
                return .none
                
            case .checkoutButtonTapped:
                state.isCheckingOut = true
                state.errorMessage = nil
                return .run { [items = state.items] send in
                    // 异步副作用(网络请求等)
                    let result = await Result {
                        try await orderClient.createOrder(items)
                    }
                    await send(.checkoutResponse(result))
                }
                
            case .checkoutResponse(.success(let order)):
                state.isCheckingOut = false
                state.orderResult = order
                state.items = []
                return .none
                
            case .checkoutResponse(.failure(let error)):
                state.isCheckingOut = false
                state.errorMessage = error.localizedDescription
                return .none
                
            case .dismissError:
                state.errorMessage = nil
                return .none
            }
        }
    }
}

// ② View 层(绑定 Store)
struct ShoppingCartView: View {
    @Bindable var store: StoreOf<ShoppingCartFeature>
    
    var body: some View {
        NavigationStack {
            Group {
                if store.isEmpty {
                    emptyState
                } else {
                    cartList
                }
            }
            .navigationTitle("购物车")
            .toolbar {
                if !store.isEmpty {
                    Button("清空") { store.send(.clearCart) }
                }
            }
        }
        .alert("结算失败", isPresented: .constant(store.errorMessage != nil)) {
            Button("确定") { store.send(.dismissError) }
        } message: {
            Text(store.errorMessage ?? "")
        }
    }
    
    var cartList: some View {
        List {
            ForEach(store.items) { item in
                CartItemRow(item: item) { newQty in
                    store.send(.updateQuantity(id: item.id, quantity: newQty))
                }
                .swipeActions {
                    Button(role: .destructive) {
                        store.send(.removeItem(id: item.id))
                    } label: { Label("删除", systemImage: "trash") }
                }
            }
            
            Section {
                HStack {
                    Text("合计").font(.headline)
                    Spacer()
                    Text("¥\(store.totalPrice, specifier: "%.2f")")
                        .font(.title3.bold())
                        .foregroundStyle(.red)
                }
            }
        }
        .safeAreaInset(edge: .bottom) {
            Button {
                store.send(.checkoutButtonTapped)
            } label: {
                Group {
                    if store.isCheckingOut {
                        ProgressView().tint(.white)
                    } else {
                        Text("立即结算")
                    }
                }
                .frame(maxWidth: .infinity)
                .frame(height: 50)
                .background(.blue)
                .foregroundStyle(.white)
                .cornerRadius(25)
            }
            .disabled(store.isCheckingOut)
            .padding()
        }
    }
    
    var emptyState: some View {
        VStack(spacing: 20) {
            Image(systemName: "cart")
                .font(.system(size: 80))
                .foregroundStyle(.secondary)
            Text("购物车是空的").font(.title3).foregroundStyle(.secondary)
        }
    }
}

// ③ 使用 Store(类比 BlocProvider)
struct ContentView: View {
    var body: some View {
        ShoppingCartView(
            store: Store(initialState: ShoppingCartFeature.State()) {
                ShoppingCartFeature()
                    ._printChanges()  // 调试:打印每次状态变化
            }
        )
    }
}

// ④ 单元测试
@MainActor
func testAddItem() async {
    let store = TestStore(
        initialState: ShoppingCartFeature.State()
    ) {
        ShoppingCartFeature()
    }
    
    let item = CartItem.mock
    
    await store.send(.addItem(item)) {
        $0.items = [item]           // 验证 state 变化
    }
    
    await store.send(.addItem(item)) {
        $0.items[0].quantity = 2    // 验证数量累加
    }
}

状态管理方案选型

场景 推荐方案 原因
单个视图的 UI 状态(展开/收起、动画等) @State 轻量直接
父子视图双向数据 @Binding 简单有效
跨多个视图共享可变状态 @Observable iOS 17 现代最佳实践
全局数据(用户信息、主题等) @Observable + .environment() 依赖注入
搜索防抖、事件流处理 Combine 响应式流处理
大型商业项目、需要严格测试 TCA 完全可预测、可测试

Demo 说明

文件 演示内容
StateBindingDemo.swift @State 计数器 + @Binding 双向控制
ObservableDemo.swift @Observable ViewModel 文章列表
CombineDemo.swift 表单联动验证 + 搜索防抖
TCADemo.swift TCA 购物车完整实现

📎 扩展内容补充

来源:第五章_网络与数据.md
本章概述:掌握 iOS 的网络请求(URLSession + async/await)、JSON 序列化(Codable)、本地存储(UserDefaults/SwiftData)、WebSocket 实时通信、图片缓存等数据层核心技能。


Flutter vs iOS 数据层对照

Flutter iOS 说明
http / Dio URLSession + async/await HTTP 网络请求
json_serializable Codable JSON 序列化/反序列化
SharedPreferences UserDefaults 键值本地存储
Hive / sqflite SwiftData / CoreData 本地数据库
WebSocket URLSessionWebSocketTask WebSocket
CachedNetworkImage AsyncImage / Kingfisher 图片网络加载

5.1 URLSession + async/await 网络请求

概念讲解

swift 复制代码
import Foundation

// 网络层封装(类比 Dio + Interceptor)
actor NetworkClient {
    static let shared = NetworkClient()
    
    private let session: URLSession
    private let baseURL = "https://api.example.com"
    
    init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 60
        self.session = URLSession(configuration: config)
    }
    
    // 通用 GET 请求
    func get<T: Decodable>(_ path: String, 
                            queryItems: [URLQueryItem]? = nil) async throws -> T {
        var components = URLComponents(string: baseURL + path)!
        components.queryItems = queryItems
        
        var request = URLRequest(url: components.url!)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(AuthManager.shared.token ?? "")", 
                         forHTTPHeaderField: "Authorization")
        
        let (data, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }
        
        guard (200...299).contains(httpResponse.statusCode) else {
            throw NetworkError.serverError(httpResponse.statusCode)
        }
        
        return try JSONDecoder.apiDecoder.decode(T.self, from: data)
    }
    
    // 通用 POST 请求
    func post<T: Decodable, B: Encodable>(_ path: String, body: B) async throws -> T {
        var request = URLRequest(url: URL(string: baseURL + path)!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(body)
        
        let (data, _) = try await session.data(for: request)
        return try JSONDecoder.apiDecoder.decode(T.self, from: data)
    }
    
    // 文件上传(multipart/form-data)
    func upload(fileURL: URL, to path: String) async throws -> UploadResult {
        var request = URLRequest(url: URL(string: baseURL + path)!)
        request.httpMethod = "POST"
        
        let boundary = UUID().uuidString
        request.setValue("multipart/form-data; boundary=\(boundary)", 
                         forHTTPHeaderField: "Content-Type")
        
        let fileData = try Data(contentsOf: fileURL)
        var body = Data()
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileURL.lastPathComponent)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
        body.append(fileData)
        body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
        request.httpBody = body
        
        let (data, _) = try await session.data(for: request)
        return try JSONDecoder().decode(UploadResult.self, from: data)
    }
    
    // 请求取消
    func fetchWithCancellation<T: Decodable>(_ path: String) async throws -> T {
        return try await withTaskCancellationHandler {
            try await get(path)
        } onCancel: {
            print("请求已取消")
        }
    }
}

// 使用示例
@Observable
class ArticleViewModel {
    var articles: [Article] = []
    var isLoading = false
    var errorMessage: String?
    
    func loadArticles() async {
        isLoading = true
        do {
            articles = try await NetworkClient.shared.get("/articles")
        } catch let error as NetworkError {
            errorMessage = error.errorDescription
        } catch {
            errorMessage = "未知错误:\(error.localizedDescription)"
        }
        isLoading = false
    }
}

项目中的应用NetworkClient 封装所有 HTTP 请求,ViewModel 调用时只关心业务逻辑。


5.2 Codable - JSON 序列化

概念讲解

Codable = Encodable + Decodable,等同于 Flutter 的 json_serializable

swift 复制代码
// 基础模型(比 json_serializable 简单,无需代码生成)
struct Article: Codable, Identifiable {
    let id: Int
    let title: String
    let content: String
    let author: Author
    let tags: [String]
    let publishedAt: Date
    var isLiked: Bool
    
    // JSON 字段名映射(类比 @JsonKey(name: 'published_at'))
    enum CodingKeys: String, CodingKey {
        case id, title, content, author, tags
        case publishedAt = "published_at"
        case isLiked = "is_liked"
    }
}

struct Author: Codable {
    let id: Int
    let name: String
    let avatarURL: URL?
    
    enum CodingKeys: String, CodingKey {
        case id, name
        case avatarURL = "avatar_url"
    }
}

// 自定义日期格式
extension JSONDecoder {
    static var apiDecoder: JSONDecoder {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        decoder.keyDecodingStrategy = .convertFromSnakeCase  // 自动驼峰转换
        return decoder
    }
}

// 嵌套 JSON / 复杂结构
struct ApiResponse<T: Codable>: Codable {
    let code: Int
    let message: String
    let data: T?
    let pagination: Pagination?
    
    var isSuccess: Bool { code == 200 }
}

struct Pagination: Codable {
    let currentPage: Int
    let totalPages: Int
    let totalCount: Int
    let perPage: Int
    
    var hasNextPage: Bool { currentPage < totalPages }
}

// 使用
let json = """
{
  "code": 200,
  "message": "success",
  "data": { "id": 1, "title": "SwiftUI入门", ... }
}
""".data(using: .utf8)!

let response = try JSONDecoder.apiDecoder.decode(
    ApiResponse<Article>.self, from: json
)

5.3 本地存储

UserDefaults - 键值存储

swift 复制代码
// 类型安全的 UserDefaults 封装(类比 Flutter 的 SharedPreferences)
@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    let storage: UserDefaults
    
    init(_ key: String, defaultValue: T, storage: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.storage = storage
    }
    
    var wrappedValue: T {
        get { storage.object(forKey: key) as? T ?? defaultValue }
        set { storage.set(newValue, forKey: key) }
    }
}

// 使用 property wrapper
struct AppSettings {
    @UserDefault("isDarkMode", defaultValue: false)
    static var isDarkMode: Bool
    
    @UserDefault("fontSize", defaultValue: 16.0)
    static var fontSize: Double
    
    @UserDefault("authToken", defaultValue: "")
    static var authToken: String
    
    @UserDefault("selectedLanguage", defaultValue: "zh-Hans")
    static var selectedLanguage: String
}

// 存储复杂对象
struct UserProfile: Codable {
    let id: String
    let name: String
    let email: String
}

extension UserDefaults {
    func saveProfile(_ profile: UserProfile) {
        if let data = try? JSONEncoder().encode(profile) {
            set(data, forKey: "userProfile")
        }
    }
    
    func loadProfile() -> UserProfile? {
        guard let data = data(forKey: "userProfile") else { return nil }
        return try? JSONDecoder().decode(UserProfile.self, from: data)
    }
}

SwiftData - 现代本地数据库(iOS 17)

swift 复制代码
import SwiftData

// 定义数据模型(类比 Flutter 的 Hive Model)
@Model
class TaskItem {
    var id: UUID
    var title: String
    var isCompleted: Bool
    var priority: Priority
    var createdAt: Date
    var tags: [String]
    
    // 关联关系
    @Relationship(deleteRule: .cascade)
    var subtasks: [SubTask] = []
    
    init(title: String, priority: Priority = .medium) {
        self.id = UUID()
        self.title = title
        self.isCompleted = false
        self.priority = priority
        self.createdAt = Date()
        self.tags = []
    }
}

enum Priority: String, Codable {
    case low = "低"
    case medium = "中"
    case high = "高"
}

// 在 App 入口配置
@main
struct iOSDemosApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: TaskItem.self)  // 注入数据容器
    }
}

// 在视图中使用
struct TaskListView: View {
    @Environment(\.modelContext) private var context
    
    // 查询(类比 Hive box.values / sqflite query)
    @Query(sort: \TaskItem.createdAt, order: .reverse)
    private var tasks: [TaskItem]
    
    // 带过滤的查询
    @Query(filter: #Predicate<TaskItem> { $0.priority == .high },
           sort: \TaskItem.createdAt)
    private var highPriorityTasks: [TaskItem]
    
    var body: some View {
        List(tasks) { task in
            TaskRowView(task: task)
        }
        .toolbar {
            Button("添加") {
                let newTask = TaskItem(title: "新任务 \(tasks.count + 1)")
                context.insert(newTask)  // 插入数据(自动持久化)
            }
        }
    }
}

// CRUD 操作
func updateTask(_ task: TaskItem, title: String) {
    task.title = title  // 直接修改属性,SwiftData 自动追踪变化
}

func deleteTask(_ task: TaskItem, context: ModelContext) {
    context.delete(task)
}

func fetchCompletedTasks(context: ModelContext) throws -> [TaskItem] {
    let descriptor = FetchDescriptor<TaskItem>(
        predicate: #Predicate { $0.isCompleted == true },
        sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
    )
    return try context.fetch(descriptor)
}

5.4 WebSocket 实时通信

概念讲解

swift 复制代码
// WebSocket 管理器(类比 Flutter 的 web_socket_channel)
@Observable
class WebSocketManager {
    var messages: [ChatMessage] = []
    var connectionState: ConnectionState = .disconnected
    
    private var webSocketTask: URLSessionWebSocketTask?
    private var heartbeatTimer: Timer?
    
    enum ConnectionState {
        case connecting, connected, disconnected
    }
    
    func connect(to urlString: String) {
        guard let url = URL(string: urlString) else { return }
        connectionState = .connecting
        
        let session = URLSession(configuration: .default)
        webSocketTask = session.webSocketTask(with: url)
        webSocketTask?.resume()
        
        connectionState = .connected
        startReceiving()
        startHeartbeat()
    }
    
    // 接收消息(循环接收)
    private func startReceiving() {
        webSocketTask?.receive { [weak self] result in
            switch result {
            case .success(let message):
                switch message {
                case .string(let text):
                    if let chatMsg = try? JSONDecoder().decode(
                        ChatMessage.self, from: text.data(using: .utf8)!
                    ) {
                        DispatchQueue.main.async {
                            self?.messages.append(chatMsg)
                        }
                    }
                case .data(let data):
                    print("收到二进制数据:\(data.count) bytes")
                @unknown default:
                    break
                }
                self?.startReceiving()  // 继续接收下一条
                
            case .failure(let error):
                print("接收失败:\(error)")
                self?.reconnect()
            }
        }
    }
    
    // 发送消息
    func send(message: String) {
        let task = URLSessionWebSocketTask.Message.string(message)
        webSocketTask?.send(task) { error in
            if let error { print("发送失败:\(error)") }
        }
    }
    
    // 心跳
    private func startHeartbeat() {
        heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { _ in
            self.webSocketTask?.sendPing { error in
                if let error {
                    print("心跳失败:\(error)")
                    self.reconnect()
                }
            }
        }
    }
    
    // 断线重连
    private func reconnect() {
        connectionState = .disconnected
        heartbeatTimer?.invalidate()
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            self.connect(to: "wss://api.example.com/ws")
        }
    }
    
    func disconnect() {
        heartbeatTimer?.invalidate()
        webSocketTask?.cancel(with: .goingAway, reason: nil)
        connectionState = .disconnected
    }
}

5.5 图片与资源管理

概念讲解

swift 复制代码
// AsyncImage - 网络图片(内置,iOS 15+)
AsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in
    switch phase {
    case .empty:
        ProgressView()  // 加载中
            .frame(width: 200, height: 200)
    case .success(let image):
        image
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(width: 200, height: 200)
            .clipped()
    case .failure:
        Image(systemName: "photo.fill")  // 加载失败
            .foregroundStyle(.secondary)
    @unknown default:
        EmptyView()
    }
}

// Kingfisher - 高性能图片缓存(类比 CachedNetworkImage)
// 添加依赖:https://github.com/onevcat/Kingfisher
import Kingfisher

struct CachedImageView: View {
    let url: URL?
    
    var body: some View {
        KFImage(url)
            .placeholder { ProgressView() }
            .fade(duration: 0.3)           // 淡入动画
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(width: 100, height: 100)
            .clipShape(Circle())
    }
}

// 本地 Asset 资源
Image("AppLogo")                    // Assets.xcassets 中的图片
    .resizable()
    .scaledToFit()

Image(systemName: "star.fill")      // SF Symbols
    .symbolRenderingMode(.multicolor)
    .font(.system(size: 30))

章节总结

技术 API 对应Flutter
HTTP请求 URLSession + async/await Dio
JSON序列化 Codable json_serializable
键值存储 UserDefaults SharedPreferences
本地数据库 SwiftData / CoreData Hive / sqflite
WebSocket URLSessionWebSocketTask web_socket_channel
图片缓存 AsyncImage / Kingfisher CachedNetworkImage

Demo 说明

Demo 文件 演示内容
URLSessionDemo.swift GET/POST/上传/取消请求
CodableDemo.swift JSON 解析 + 嵌套结构
LocalStorageDemo.swift UserDefaults + SwiftData CRUD
WebSocketDemo.swift 实时聊天室 + 心跳重连
ImageCacheDemo.swift AsyncImage + Kingfisher 图片缓存
相关推荐
花间相见3 小时前
【大模型微调与部署01】—— ms-swift-3.12入门:安装、快速上手
开发语言·ios·swift
空中海4 小时前
第一章:Swift 语言核心
ios·cocoa·swift
90后的晨仔6 小时前
《SwiftUI 进阶第6章:列表与滚动视图》
ios
空中海6 小时前
第十章:iOS架构设计与工程化
macos·ios·cocoa
90后的晨仔13 小时前
《SwiftUI 进阶第7章:导航系统》
ios
90后的晨仔13 小时前
《swiftUI进阶 第9章SwiftUI 状态管理完全指南》
ios
90后的晨仔13 小时前
《 SwiftUI 进阶第8章:表单与设置界面》
ios
90后的晨仔13 小时前
《SwiftUI 进阶第5章:数据处理与网络请求》
ios
90后的晨仔15 小时前
《SwiftUI 进阶第4章:响应式布局》
ios