实战:做一个待办 App
把前面章节串成一个可运行的小产品。
1目标产品
一个本地待办:列表、添加、滑动删除、完成切换、杀进程再开仍在。
建工程
→
模型+Store
→
List UI
→
持久化
→
真机验收
2模型与 Store
struct TodoItem: Identifiable, Codable, Equatable {
var id = UUID()
var title: String
var isDone: Bool = false
}
@Observable
final class TodoStore {
var items: [TodoItem] = []
private let fileName = "todos.json"
private var url: URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent(fileName)
}
func load() {
guard let data = try? Data(contentsOf: url),
let decoded = try? JSONDecoder().decode([TodoItem].self, from: data)
else { return }
items = decoded
}
func save() {
guard let data = try? JSONEncoder().encode(items) else { return }
try? data.write(to: url, options: .atomic)
}
func add(_ title: String) {
let t = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !t.isEmpty else { return }
items.insert(TodoItem(title: t), at: 0)
save()
}
func toggle(_ item: TodoItem) {
guard let i = items.firstIndex(of: item) else { return }
items[i].isDone.toggle()
save()
}
func remove(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
save()
}
}
3界面与入口
struct TodoHomeView: View {
@State private var store = TodoStore()
@State private var draft = ""
var body: some View {
NavigationStack {
List {
ForEach(store.items) { item in
HStack {
Image(systemName: item.isDone ? "checkmark.circle.fill" : "circle")
.onTapGesture { store.toggle(item) }
Text(item.title)
.strikethrough(item.isDone)
}
}
.onDelete(perform: store.remove)
}
.navigationTitle("待办")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("新任务", text: $draft)
Button("添加") {
store.add(draft)
draft = ""
}
}
}
}
.onAppear { store.load() }
}
}
}
@main
struct TodoDemoApp: App {
var body: some Scene {
WindowGroup { TodoHomeView() }
}
}
4验收清单(真实标准)
功能
增、删、勾选完成都 OK。
持久化
划掉 App 再开,数据还在。
空输入
空白标题不会添加。
真机
自己手机上走一遍。
做完这一章,你已经走过「能跑的小产品」闭环。接下来才是网络同步、账号、推送等增强。