下边是一个登录的简单示例,直接复制到iOS项目工程中运行即可,URL需要替换成真实的才能起作用。这里的简单示例仅供学习!
Swift
// Combine + MVVM 示例:一个简单的登录界面,加入真实网络请求(使用 URLSession)
import SwiftUI
import Combine
// MARK: - 网络模型
struct LoginResponse: Decodable {
let success: Bool
let message: String
}
// MARK: - ViewModel
class LoginViewModel: ObservableObject {
// 输入
@Published var username: String = ""
@Published var password: String = ""
// 输出
@Published var isLoginEnabled: Bool = false
@Published var loginStatus: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
// 验证输入是否满足登录条件(用户名和密码长度 >= 3)
Publishers.CombineLatest($username, $password)
.map { username, password in
return username.count >= 3 && password.count >= 3
}
.assign(to: &$isLoginEnabled)
}
func login() {
guard let url = URL(string: "https://example.com/api/login") else {
loginStatus = "无效的请求地址"
return
}
let params = ["username": username, "password": password]
let requestBody = try? JSONSerialization.data(withJSONObject: params)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = requestBody
loginStatus = "登录中..."
URLSession.shared.dataTaskPublisher(for: request)
.map { $0.data }
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case let .failure(error) = completion {
self.loginStatus = "请求失败:\(error.localizedDescription)"
}
}, receiveValue: { response in
self.loginStatus = response.success ? "登录成功 ✅" : "登录失败 ❌:\(response.message)"
})
.store(in: &cancellables)
}
}
// MARK: - View
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
var body: some View {
VStack(spacing: 20) {
TextField("用户名", text: $viewModel.username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.autocapitalization(.none)
SecureField("密码", text: $viewModel.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("登录") {
viewModel.login()
}
.disabled(!viewModel.isLoginEnabled)
.padding()
.background(viewModel.isLoginEnabled ? Color.blue : Color.gray)
.foregroundColor(.white)
.cornerRadius(8)
Text(viewModel.loginStatus)
.foregroundColor(.green)
}
.padding()
}
}
// MARK: - 预览
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
一、Combine 框架核心概念解析
1. 发布者 (Publisher)
swift
@Published var username: String = ""
@Published属性包装器:将普通属性转换为发布者- 当属性值改变时自动发送新值
- 本质是创建了
Publisher的派生类型:Published<Value>.Publisher
2. 操作符 (Operators)
swift
Publishers.CombineLatest($username, $password)
.map { ... } // 转换操作符
.assign(to: &$isLoginEnabled) // 绑定操作符
CombineLatest:合并两个发布者的最新值(此处监听两个输入框)map:值转换(将元组转换为布尔值)assign:将值直接绑定到属性(自动管理内存)
3. 订阅者 (Subscriber)
swift
.sink(receiveCompletion: { ... }, receiveValue: { ... })
sink:最常用的订阅者,提供两个闭包:receiveValue:处理正常数据流receiveCompletion:处理完成事件(成功/失败)
- 返回
AnyCancellable对象
4. 订阅管理
swift
private var cancellables = Set<AnyCancellable>()
.store(in: &cancellables)
AnyCancellable:类型擦除的取消令牌- 内存管理原理 :
- 当
cancellables集合被释放时,自动取消所有订阅 - 防止内存泄漏(尤其重要网络请求场景)
- 当
二、MVVM 架构实现细节
1. ViewModel 设计
swift
class LoginViewModel: ObservableObject {
// 输入属性
@Published var username = ""
@Published var password = ""
// 输出属性
@Published var isLoginEnabled = false
@Published var loginStatus = ""
}
-
数据流向 :
graph LR View-->|绑定| ViewModel-->|发布| View -
双向绑定 :
@Published属性 + SwiftUI 的$前缀语法 -
状态驱动:所有 UI 变化都源自状态改变
2. 输入验证的实现
swift
Publishers.CombineLatest($username, $password)
.map { u, p in u.count >= 3 && p.count >= 3 }
.assign(to: &$isLoginEnabled)
- 响应式验证:当任一输入变化时自动重新验证
- 无副作用:纯函数式转换(map 内部无状态修改)
三、网络请求关键实现
1. URLSession 的 Combine 扩展
swift
URLSession.shared.dataTaskPublisher(for: request)
- 返回
DataTaskPublisher类型(遵守Publisher协议) - 发出值类型:
(data: Data, response: URLResponse) - 错误类型:
URLError
2. 响应处理链
swift
.map { $0.data } // 提取数据
.decode(type: LoginResponseModel.self, decoder: JSONDecoder()) // JSON解码
.receive(on: DispatchQueue.main) // 切换主线程
.sink(...) // 处理结果
- 操作符链:典型的数据处理管道(Pipeline)
- 线程切换 :
- 网络请求默认在后台线程
receive(on:)确保 UI 更新在主线程
3. 错误处理机制
swift
sink(receiveCompletion: {
if case .failure(let error) = completion {
self.loginStatus = "失败: \(error.localizedDescription)"
}
}, ...)
- 分离处理 :成功走
receiveValue,失败走receiveCompletion - 错误转换:保留原始错误信息(开发时可扩展为自定义错误类型)
四、SwiftUI 视图层实现要点
1. 视图与 ViewModel 的连接
swift
@StateObject private var viewModel = LoginViewModel()
@StateObject:视图持有 ViewModel 的生命周期- 与
@ObservedObject的区别:创建者负责管理内存
2. 控件绑定
swift
TextField("用户名", text: $viewModel.username)
SecureField("密码", text: $viewModel.password)
$语法糖 :访问@Published属性的投影(Projection)- 等价于:
Binding<String>类型
3. 条件渲染
swift
.background(viewModel.isLoginEnabled ? Color.blue : Color.gray)
.disabled(!viewModel.isLoginEnabled)
- 响应式 UI:属性变化自动触发视图更新
- 无命令式代码 :无需手动
reload或update
五、关键优化点与注意事项
-
网络请求取消
swift// 在 ViewModel 中增加 func cancelRequests() { cancellables.forEach { $0.cancel() } }- 在视图消失时调用(防止后台更新销毁的视图)
-
输入防抖处理
swift.debounce(for: .seconds(0.5), scheduler: RunLoop.main)- 避免频繁触发验证(尤其快速输入场景)
-
错误分类处理
swiftenum LoginError: Error { case invalidURL, networkError(URLError), decodingError }- 使用自定义错误类型提升可维护性
-
测试方案
- ViewModel 单元测试:Mock URLSession
- UI 测试:验证不同状态下的界面表现
六、MVVM + Combine 优势总结
| 特性 | 传统方式 | Combine + MVVM |
|---|---|---|
| 数据绑定 | 手动更新 | 自动响应 |
| 异步处理 | 回调地狱 | 声明式管道 |
| 状态管理 | 分散控制 | 集中处理 |
| 代码量 | 冗余 | 简洁(减少30%+) |
| 可测试性 | 困难 | 依赖注入友好 |
完整技术栈:SwiftUI 声明式UI + Combine 响应式编程 + MVVM 架构模式
这个示例完整展示了现代 Swift 开发的核心理念:通过声明式语法描述 UI,通过响应式编程处理数据流,通过架构模式解耦逻辑。