1. SwiftUI 与 ViewModel 的协作模式
在 iOS 开发领域,SwiftUI 的声明式语法彻底改变了界面构建方式。但当我们把 MVVM 架构引入 SwiftUI 时,ViewModel 的角色定位常常引发争议。与 UIKit 时代不同,SwiftUI 的 @State 和 @ObservedObject 等属性包装器已经承担了部分数据管理职责,这让我们不得不重新思考:ViewModel 在 SwiftUI 中究竟该管什么?
典型的误区是把 UIKit 的 MVVM 模式直接套用到 SwiftUI。我曾见过有开发者将所有 @State 变量机械地迁移到 ViewModel 中,结果导致代码复杂度不降反升。实际上,SwiftUI 的 View 结构本身就应该包含界面相关的临时状态,比如当前选中的 Tab 索引或动画进度值。这些状态如果强行放到 ViewModel 里,反而会破坏代码的可维护性。
1.1 ViewModel 的合理职责边界
经过多个项目的实践验证,我认为 ViewModel 在 SwiftUI 中应该专注以下核心职责:
- 业务逻辑封装:处理网络请求、数据转换、表单验证等纯业务操作
- 跨视图状态共享:管理多个视图需要访问的共享数据
- 依赖项整合:协调多个 Service 或 Repository 的协作
- 测试隔离层:为视图提供可 mock 的抽象接口
举个例子,在开发一个社交媒体应用时,用户个人主页的 ViewModel 应该负责:
swift复制class ProfileViewModel: ObservableObject {
@Published var posts: [Post] = []
@Published var isLoading = false
private let userService: UserServiceProtocol
private let postService: PostServiceProtocol
func loadUserData(userId: String) async {
isLoading = true
do {
let userPosts = try await postService.fetchPosts(for: userId)
posts = userPosts.sorted { $0.createdAt > $1.createdAt }
} catch {
print("加载失败: \(error)")
}
isLoading = false
}
}
而界面相关的状态,比如是否显示点赞动画、当前滚动位置等,则应该保留在 View 结构中。
1.2 属性包装器的选用策略
SwiftUI 提供了多种数据管理工具,选择不当会导致不必要的视图刷新:
- @State:视图私有状态,适合文本框内容、开关状态等临时UI状态
- @ObservedObject:外部可观察对象,通常用于 ViewModel
- @StateObject:与 @ObservedObject 类似,但保证生命周期与视图一致
- @EnvironmentObject:跨视图层级共享数据
在最近的一个电商项目中,我们因为错误使用 @ObservedObject 导致商品列表频繁重载。后来通过改用 @StateObject 解决了问题:
swift复制struct ProductListView: View {
@StateObject var viewModel = ProductListViewModel() // 正确用法
var body: some View {
List(viewModel.products) { product in
ProductRow(product: product)
}
}
}
关键经验:当 ViewModel 的初始化由当前视图负责时,必须使用 @StateObject 而非 @ObservedObject,否则每次视图更新都会创建新的 ViewModel 实例。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ViewModel 的线程安全实践
SwiftUI 的视图更新必须在主线程进行,但 ViewModel 常常需要处理后台任务。我曾在一个金融类 App 中遇到因线程问题导致的界面卡死,最终通过以下模式解决:
2.1 异步操作的标准范式
swift复制class TransactionViewModel: ObservableObject {
@Published var transactions: [Transaction] = []
@Published var error: Error?
private let service: TransactionService
func loadTransactions() async {
do {
let result = try await service.fetchTransactions()
await MainActor.run { // 确保在主线程更新
transactions = result
}
} catch {
await MainActor.run {
self.error = error
}
}
}
}
2.2 Combine 与 async/await 的混合使用
在 iOS 15+ 项目中,推荐使用 async/await 处理异步流程。但对于需要持续更新的数据流(如实时股价),Combine 仍然是更好的选择:
swift复制class StockViewModel: ObservableObject {
@Published var price: Double = 0.0
private var cancellables = Set<AnyCancellable>()
init(stockService: StockService) {
stockService.realtimePricePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] newPrice in
self?.price = newPrice
}
.store(in: &cancellables)
}
}
3. 复杂表单的数据管理
处理多字段表单是 ViewModel 的典型场景。在最近开发的健康问卷模块中,我总结出以下最佳实践:
3.1 表单状态建模
swift复制class HealthFormViewModel: ObservableObject {
struct FormData {
var name: String = ""
var age: Int?
var symptoms: Set<Symptom> = []
// 其他表单字段...
}
@Published var formData = FormData()
@Published var validationErrors: [String: String] = [:]
func validate() -> Bool {
var isValid = true
validationErrors.removeAll()
if formData.name.isEmpty {
validationErrors["name"] = "请输入姓名"
isValid = false
}
if formData.age == nil {
validationErrors["age"] = "请输入有效年龄"
isValid = false
}
return isValid
}
}
3.2 与 SwiftUI 表单的集成技巧
swift复制struct HealthFormView: View {
@StateObject var viewModel = HealthFormViewModel()
var body: some View {
Form {
TextField("姓名", text: $viewModel.formData.name)
.background(viewModel.validationErrors["name"] != nil ? Color.red.opacity(0.1) : Color.clear)
if let error = viewModel.validationErrors["name"] {
Text(error).foregroundColor(.red)
}
// 其他表单字段...
}
}
}
4. 测试驱动开发实践
良好的 ViewModel 设计应该便于单元测试。以下是测试购物车 ViewModel 的示例:
4.1 测试用例设计
swift复制class CartViewModelTests: XCTestCase {
var mockService: MockCartService!
var viewModel: CartViewModel!
override func setUp() {
mockService = MockCartService()
viewModel = CartViewModel(service: mockService)
}
func testAddItem() async {
let testItem = CartItem(id: "1", name: "测试商品", price: 100)
mockService.addItemResult = .success(testItem)
await viewModel.addItem(item: testItem)
XCTAssertEqual(viewModel.items.count, 1)
XCTAssertEqual(viewModel.totalPrice, 100)
}
func testCheckoutFailure() async {
mockService.checkoutResult = .failure(NetworkError.timeout)
await viewModel.checkout()
XCTAssertTrue(viewModel.showError)
XCTAssertEqual(viewModel.errorMessage, "请求超时")
}
}
4.2 依赖注入模式
为了实现可测试性,ViewModel 应该通过协议接收依赖项:
swift复制protocol CartServiceProtocol {
func addItem(_ item: CartItem) async -> Result<CartItem, Error>
func checkout() async -> Result<Order, Error>
}
class CartViewModel: ObservableObject {
private let service: CartServiceProtocol
init(service: CartServiceProtocol) {
self.service = service
}
// ...业务方法...
}
5. 性能优化关键点
在开发大型列表视图时,不当的 ViewModel 设计会导致严重性能问题。以下是经过实战验证的优化方案:
5.1 数据分页加载模式
swift复制class FeedViewModel: ObservableObject {
@Published var items: [FeedItem] = []
private var currentPage = 0
private var isLoading = false
func loadNextPage() async {
guard !isLoading else { return }
isLoading = true
defer { isLoading = false }
do {
let newItems = try await FeedService.fetchFeed(page: currentPage)
await MainActor.run {
items.append(contentsOf: newItems)
currentPage += 1
}
} catch {
// 错误处理
}
}
}
5.2 避免不必要的视图刷新
使用 Equatable 协议减少深层嵌套数据的无效刷新:
swift复制struct UserProfile: Equatable {
let id: String
let name: String
let avatarURL: URL?
// 其他字段...
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.avatarURL == rhs.avatarURL
}
}
class ProfileViewModel: ObservableObject {
@Published var profile: UserProfile? // 只有真正变化时才会触发视图更新
}
6. 架构演进建议
随着项目规模扩大,基础 ViewModel 可能需要进行功能拆分:
6.1 功能模块化方案
swift复制// 核心协议
protocol ViewModelProtocol: ObservableObject {
associatedtype State
associatedtype Action
var state: State { get }
func send(_ action: Action)
}
// 具体实现
class SearchViewModel: ViewModelProtocol {
enum Action {
case search(query: String)
case loadMore
}
struct State {
var results: [SearchResult] = []
var isLoading = false
}
@Published private(set) var state = State()
func send(_ action: Action) {
switch action {
case .search(let query):
Task { await search(query: query) }
case .loadMore:
Task { await loadMore() }
}
}
private func search(query: String) async {
// 实现搜索逻辑
}
}
6.2 与 Coordinator 模式的结合
对于复杂导航逻辑,可以将 ViewModel 与路由逻辑分离:
swift复制class ArticleListViewModel: ObservableObject {
private let coordinator: ArticleCoordinator
init(coordinator: ArticleCoordinator) {
self.coordinator = coordinator
}
func showDetail(for article: Article) {
coordinator.showArticleDetail(article)
}
}
在实际项目中,我发现这种分层设计特别适合有复杂用户流程的电商类应用。ViewModel 只需关注业务数据处理,而导航逻辑则由专门的 Coordinator 处理,大大降低了模块间的耦合度。
