本章系统讲解 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 图片缓存 |