You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用SwiftUI与Combine的应用如何实现ViewModel的Mock?

SwiftUI + Combine 环境下 Mock ViewModel 的实现方案

你遇到的@Published无法直接在协议中声明的问题,可以通过协议声明对应Publisher的方式绕开,完整实现方案如下:


1. 基于协议的完整Mock方案

该方案可以完全解耦View和具体ViewModel实现,同时支持单元测试验证方法调用、状态变更。

@Published 是属性包装器,本身无法直接定义在协议中,但我们可以在协议中声明对应属性的 Publisher 类型来满足依赖抽象的要求。

步骤1:定义ViewModel协议

协议必须继承ObservableObject,只需要声明对外暴露的属性、对应Publisher以及业务方法即可:

protocol UserViewModelProtocol: ObservableObject {
    // 对外可读写的状态属性
    var username: String { get set }
    var isLoading: Bool { get set }
    // 对应@Published生成的Publisher
    var usernamePublisher: Published<String>.Publisher { get }
    var isLoadingPublisher: Published<Bool>.Publisher { get }
    // 业务方法
    func fetchUserInfo()
}

步骤2:让原业务ViewModel遵守协议

不需要修改原有业务代码,只需要扩展实现协议要求的Publisher属性即可,@Published修饰的属性会自动生成带$前缀的Publisher,直接返回即可:

// 原有真实业务ViewModel
class UserViewModel: ObservableObject {
    @Published var username: String = ""
    @Published var isLoading: Bool = false
    
    func fetchUserInfo() {
        // 原有真实网络请求、业务逻辑
        isLoading = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.isLoading = false
            self.username = "真实用户"
        }
    }
}

// 扩展遵守协议
extension UserViewModel: UserViewModelProtocol {
    var usernamePublisher: Published<String>.Publisher { $username }
    var isLoadingPublisher: Published<Bool>.Publisher { $isLoading }
}

步骤3:实现Mock版本ViewModel

直接遵守协议,按需模拟状态和方法行为即可,还可以添加标记验证方法调用情况:

class MockUserViewModel: UserViewModelProtocol {
    @Published var username: String = "默认模拟用户"
    @Published var isLoading: Bool = false
    
    var usernamePublisher: Published<String>.Publisher { $username }
    var isLoadingPublisher: Published<Bool>.Publisher { $isLoading }
    
    // 标记方法是否被调用,用于单元测试验证
    var fetchUserInfoIsCalled = false
    func fetchUserInfo() {
        fetchUserInfoIsCalled = true
        // 直接返回模拟结果,不需要执行真实逻辑
        isLoading = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.isLoading = false
            self.username = "模拟请求返回的用户名"
        }
    }
}

步骤4:View层适配

将View的ViewModel改为泛型,依赖协议而非具体实现:

struct UserView<T: UserViewModelProtocol>: View {
    @StateObject var viewModel: T
    
    var body: some View {
        VStack(spacing: 20) {
            if viewModel.isLoading {
                ProgressView("加载中")
            } else {
                Text("用户名:\(viewModel.username)")
            }
            Button("获取用户信息") {
                viewModel.fetchUserInfo()
            }
        }
    }
}

// 正式环境使用
// UserView(viewModel: UserViewModel())

// 测试/预览环境使用
// UserView(viewModel: MockUserViewModel())

轻量替代方案(仅适用于预览/简单Mock场景)

如果不需要做单元测试验证方法调用,只是为了SwiftUI预览快速构造假数据,可以直接继承原有ViewModel重写对应方法,不需要写协议:

class PreviewUserViewModel: UserViewModel {
    override func fetchUserInfo() {
        // 跳过真实请求,直接赋值模拟数据
        self.isLoading = false
        self.username = "预览专用用户名"
    }
}

// 预览时使用
// UserView(viewModel: PreviewUserViewModel())

内容的提问来源于stack exchange,提问作者Edwin ZAP

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.09 22:09:01