综合练习
Todo App(SwiftUI + URLSession)。
40 分钟
综合练习: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)
}
}