大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
综合练习:Todo App
用本 track 所学完成 Todo App:SwiftUI 界面 + MVVM + URLSession(或本地 Core Data)+ 单元测试。对照 /roadmaps/ios 全路径验收。
学习目标
- 完成可运行的 Todo 应用(增删改查 + 完成切换)
- 实践 MVVM 分层与 Repository 抽象
- 可选对接 Spring Boot API 或纯本地持久化
- 规划 TestFlight 或下一步 Swift 进阶
核心概念
项目结构
CodeRoadMapTodo/
├── App/
│ └── CodeRoadMapTodoApp.swift
├── Models/
│ └── TodoItem.swift
├── ViewModels/
│ └── TodoListViewModel.swift
├── Views/
│ ├── TodoListView.swift
│ └── TodoRowView.swift
├── Services/
│ ├── TodoRepository.swift
│ └── APIClient.swift
└── Tests/
└── TodoListViewModelTests.swift
Model 与 ViewModel
struct TodoItem: Identifiable, Codable, Equatable {
let id: UUID
var title: String
var isCompleted: Bool
var createdAt: Date
}
@MainActor
final class TodoListViewModel: ObservableObject {
@Published var items: [TodoItem] = []
@Published var newTitle = ""
@Published var isLoading = false
@Published var errorMessage: String?
private let repository: TodoRepositoryProtocol
init(repository: TodoRepositoryProtocol = TodoRepository()) {
self.repository = repository
}
func load() async {
isLoading = true
do {
items = try await repository.fetchAll()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func addTodo() async {
let title = newTitle.trimmingCharacters(in: .whitespaces)
guard !title.isEmpty else { return }
let item = TodoItem(id: UUID(), title: title, isCompleted: false, createdAt: .now)
do {
try await repository.save(item)
items.append(item)
newTitle = ""
} catch {
errorMessage = error.localizedDescription
}
}
func toggle(_ item: TodoItem) async {
var updated = item
updated.isCompleted.toggle()
try? await repository.save(updated)
if let idx = items.firstIndex(where: { $0.id == item.id }) {
items[idx] = updated
}
}
func delete(at offsets: IndexSet) async {
for index in offsets {
let item = items[index]
try? await repository.delete(item)
}
items.remove(atOffsets: offsets)
}
}
SwiftUI 主界面
struct TodoListView: View {
@StateObject private var viewModel = TodoListViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("加载中…")
} else {
List {
ForEach(viewModel.items) { item in
TodoRowView(item: item) {
Task { await viewModel.toggle(item) }
}
}
.onDelete { offsets in
Task { await viewModel.delete(at: offsets) }
}
}
}
}
.navigationTitle("待办")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
EditButton()
}
}
.safeAreaInset(edge: .bottom) {
HStack {
TextField("新待办", text: $viewModel.newTitle)
.textFieldStyle(.roundedBorder)
Button("添加") {
Task { await viewModel.addTodo() }
}
.disabled(viewModel.newTitle.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding()
}
.task { await viewModel.load() }
.alert("错误", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("确定") { viewModel.errorMessage = nil }
} message: {
Text(viewModel.errorMessage ?? "")
}
}
}
}
功能清单
| 功能 | 要求 |
|---|---|
| 列表展示 | List + 完成态图标 |
| 新增 | 底部输入框 + 添加按钮 |
| 切换完成 | 点击行或 checkbox |
| 删除 | 左滑或 Edit 模式 |
| 持久化 | Core Data 或 REST API |
| 测试 | ViewModel 至少 2 个单元测试 |
与 Java/Spring 或 React 对照
| 本练习 | 可选后端 | Web 等价 |
|---|---|---|
| TodoRepository | Spring @RestController CRUD | React Todo + fetch |
| MVVM ViewModel | @Service | Custom Hook |
| SwiftUI List | — | <ul> + map |
| XCTest | @WebMvcTest | Vitest |
选做:用 spring-boot-essentials 实现 /api/todos,iOS 端 URLSession 联调。
练习清单
- 完成上述 Todo App,模拟器可运行
- 编写 ViewModel 成功/失败单元测试
- 在 /roadmaps/ios 标记已学节点
- 可选:Archive 上传 TestFlight
小结
iOS 核心在本 track 闭环:Swift → SwiftUI → MVVM → 网络/存储 → 测试 → 上架。 下一步可深入 Swift 进阶、Combine 或读 Apple 官方 Sample Code。