Tailwind CSSTailwind CSS
Home
  • Tailwind CSS 书籍目录
  • Vue 3 开发实战指南
  • React 和 Next.js 学习
  • TypeScript
  • React开发框架书籍大纲
  • Shadcn学习大纲
  • Swift 编程语言:从入门到进阶
  • SwiftUI 学习指南
  • 函数式编程大纲
  • Swift 异步编程语言
  • Swift 协议化编程
  • SwiftUI MVVM 开发模式
  • SwiftUI 图表开发书籍
  • SwiftData
  • ArkTS编程语言:从入门到精通
  • 仓颉编程语言:从入门到精通
  • 鸿蒙手机客户端开发实战
  • WPF书籍
  • C#开发书籍
learn
  • Java编程语言
  • Kotlin 编程入门与实战
  • /python/outline.html
  • AI Agent
  • MCP (Model Context Protocol) 应用指南
  • 深度学习
  • 深度学习
  • 强化学习: 理论与实践
  • 扩散模型书籍
  • Agentic AI for Everyone
langchain
Home
  • Tailwind CSS 书籍目录
  • Vue 3 开发实战指南
  • React 和 Next.js 学习
  • TypeScript
  • React开发框架书籍大纲
  • Shadcn学习大纲
  • Swift 编程语言:从入门到进阶
  • SwiftUI 学习指南
  • 函数式编程大纲
  • Swift 异步编程语言
  • Swift 协议化编程
  • SwiftUI MVVM 开发模式
  • SwiftUI 图表开发书籍
  • SwiftData
  • ArkTS编程语言:从入门到精通
  • 仓颉编程语言:从入门到精通
  • 鸿蒙手机客户端开发实战
  • WPF书籍
  • C#开发书籍
learn
  • Java编程语言
  • Kotlin 编程入门与实战
  • /python/outline.html
  • AI Agent
  • MCP (Model Context Protocol) 应用指南
  • 深度学习
  • 深度学习
  • 强化学习: 理论与实践
  • 扩散模型书籍
  • Agentic AI for Everyone
langchain
  • 案例:Todo 列表的 ViewModel 实现

案例:Todo 列表的 ViewModel 实现

1. ViewModel 的设计目标

在本案例中,我们将为 Todo 列表应用实现一个完整的 ViewModel 层,主要实现以下功能:

  • 管理 Todo 项的数据集合
  • 处理添加/删除/更新 Todo 项的逻辑
  • 提供过滤和排序功能
  • 作为 View 层和 Model 层之间的桥梁

2. 基础 ViewModel 结构

import Combine
import SwiftUI

class TodoListViewModel: ObservableObject {
    // MARK: - 发布属性
    @Published var todos: [TodoItem] = []
    @Published var filterText: String = ""
    @Published var showCompletedOnly = false
    
    // MARK: - 私有属性
    private var cancellables = Set<AnyCancellable>()
    
    // MARK: - 初始化
    init() {
        loadInitialData()
        setupBindings()
    }
}

3. 核心业务逻辑实现

3.1 数据操作方法

extension TodoListViewModel {
    func addTodo(title: String) {
        let newItem = TodoItem(
            id: UUID(),
            title: title,
            isCompleted: false,
            createdAt: Date()
        )
        todos.append(newItem)
    }
    
    func toggleCompletion(for todo: TodoItem) {
        if let index = todos.firstIndex(where: { $0.id == todo.id }) {
            todos[index].isCompleted.toggle()
        }
    }
    
    func deleteTodo(at indexSet: IndexSet) {
        todos.remove(atOffsets: indexSet)
    }
}

3.2 数据绑定与过滤

extension TodoListViewModel {
    private func setupBindings() {
        $filterText
            .combineLatest($showCompletedOnly)
            .map { [weak self] (filterText, showCompleted) in
                self?.filterTodos(text: filterText, showCompleted: showCompleted) ?? []
            }
            .assign(to: &$todos)
    }
    
    private func filterTodos(text: String, showCompleted: Bool) -> [TodoItem] {
        var filtered = todos
        
        if !text.isEmpty {
            filtered = filtered.filter { $0.title.localizedCaseInsensitiveContains(text) }
        }
        
        if showCompleted {
            filtered = filtered.filter { $0.isCompleted }
        }
        
        return filtered
    }
}

4. 与 View 层的集成

4.1 提供视图所需数据

extension TodoListViewModel {
    var completedTodosCount: Int {
        todos.filter { $0.isCompleted }.count
    }
    
    var totalTodosCount: Int {
        todos.count
    }
    
    var progressPercentage: Double {
        guard !todos.isEmpty else { return 0 }
        return Double(completedTodosCount) / Double(totalTodosCount)
    }
}

4.2 在 SwiftUI 中的使用示例

struct TodoListView: View {
    @StateObject var viewModel = TodoListViewModel()
    
    var body: some View {
        VStack {
            // 搜索和筛选控制
            SearchAndFilterView(viewModel: viewModel)
            
            // 进度显示
            ProgressView(value: viewModel.progressPercentage)
                .padding()
            
            // 列表主体
            List {
                ForEach(viewModel.todos) { todo in
                    TodoRowView(todo: todo) {
                        viewModel.toggleCompletion(for: todo)
                    }
                }
                .onDelete(perform: viewModel.deleteTodo)
            }
            
            // 添加新项目
            AddTodoView { newTitle in
                viewModel.addTodo(title: newTitle)
            }
        }
    }
}

5. 测试要点

5.1 单元测试示例

class TodoListViewModelTests: XCTestCase {
    var viewModel: TodoListViewModel!
    
    override func setUp() {
        super.setUp()
        viewModel = TodoListViewModel()
    }
    
    func testAddTodo() {
        let initialCount = viewModel.todos.count
        viewModel.addTodo(title: "Test Todo")
        XCTAssertEqual(viewModel.todos.count, initialCount + 1)
        XCTAssertEqual(viewModel.todos.last?.title, "Test Todo")
    }
    
    func testToggleCompletion() {
        viewModel.addTodo(title: "Test")
        let initialState = viewModel.todos[0].isCompleted
        viewModel.toggleCompletion(for: viewModel.todos[0])
        XCTAssertNotEqual(viewModel.todos[0].isCompleted, initialState)
    }
}

6. 实现注意事项

  1. 单一职责原则:ViewModel 只负责业务逻辑,不包含视图布局信息
  2. 不可变数据:在传递数据给 View 时使用值类型(struct)
  3. 线程安全:确保数据修改都在主线程进行
  4. 内存管理:合理使用 [weak self] 避免循环引用
  5. 可测试性:所有业务逻辑都应该可以被独立测试

7. 完整案例代码结构

TodoApp/
├── Models/
│   └── TodoItem.swift
├── ViewModels/
│   └── TodoListViewModel.swift
├── Views/
│   ├── TodoListView.swift
│   ├── TodoRowView.swift
│   └── AddTodoView.swift
└── Tests/
    └── TodoListViewModelTests.swift

通过这个案例,我们展示了如何在 SwiftUI 中实现一个符合 MVVM 模式的 ViewModel,它清晰地分离了业务逻辑和视图表现,同时保持了良好的可测试性和可维护性。

Last Updated:: 4/25/25, 8:13 PM