从一个简单的登录示例开始解析Combine + MVVM

下边是一个登录的简单示例,直接复制到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:属性变化自动触发视图更新
  • 无命令式代码 :无需手动 reloadupdate

五、关键优化点与注意事项

  1. 网络请求取消

    swift 复制代码
    // 在 ViewModel 中增加
    func cancelRequests() {
        cancellables.forEach { $0.cancel() }
    }
    • 在视图消失时调用(防止后台更新销毁的视图)
  2. 输入防抖处理

    swift 复制代码
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
    • 避免频繁触发验证(尤其快速输入场景)
  3. 错误分类处理

    swift 复制代码
    enum LoginError: Error {
        case invalidURL, networkError(URLError), decodingError
    }
    • 使用自定义错误类型提升可维护性
  4. 测试方案

    • ViewModel 单元测试:Mock URLSession
    • UI 测试:验证不同状态下的界面表现

六、MVVM + Combine 优势总结

特性 传统方式 Combine + MVVM
数据绑定 手动更新 自动响应
异步处理 回调地狱 声明式管道
状态管理 分散控制 集中处理
代码量 冗余 简洁(减少30%+)
可测试性 困难 依赖注入友好

完整技术栈:SwiftUI 声明式UI + Combine 响应式编程 + MVVM 架构模式


这个示例完整展示了现代 Swift 开发的核心理念:通过声明式语法描述 UI,通过响应式编程处理数据流,通过架构模式解耦逻辑

相关推荐
搜狐技术产品小编202319 小时前
精通 UITableViewDiffableDataSource——从入门到重构的现代 iOS 列表开发指南
ios·重构
tangweiguo0305198720 小时前
SwiftUI 状态管理完全指南:从 @State 到 @EnvironmentObject
ios
Digitally1 天前
如何轻松地将文件从 PC 传输到 iPhone
ios·iphone
iosTiov1 天前
当IPA遇见信任:解密ios生态中“签名”的真正力量
ios·团队开发·苹果签名·稳定
游戏开发爱好者81 天前
如何使用 AppUploader 提交上传 iOS 应用
android·ios·小程序·https·uni-app·iphone·webview
和沐阳学逆向1 天前
iOS 18 越狱教程:palera1n + 巨魔安装全流程
ios·巨魔商店·ios越狱·ios18越狱
ii_best2 天前
安卓/ios脚本开发辅助工具按键精灵横纵坐标转换教程
android·开发语言·ios·安卓
先飞的笨鸟2 天前
2026 年 Expo + React Native 项目接入微信分享完整指南
前端·ios·app
初级代码游戏2 天前
iOS开发 SwiftUI 5 : 文本输入 密码输入 多行输入
ios·swiftui·swift
iosTiov2 天前
ios生态的分发密钥:企业签、V3签、TF签深度解析与选型指南
安全·ios·团队开发·苹果签名·稳定