案例:实现一个简单的 Todo 数据模型
目标
通过实现一个基础的 Todo 数据模型,理解 SwiftUI 中 Model 层的设计原则和实现方式。
核心概念
1. 数据模型定义
struct TodoItem: Identifiable, Codable {
let id: UUID
var title: String
var isCompleted: Bool
var dueDate: Date?
init(title: String, isCompleted: Bool = false, dueDate: Date? = nil) {
self.id = UUID()
self.title = title
self.isCompleted = isCompleted
self.dueDate = dueDate
}
}
关键特性:
- 遵循
Identifiable协议(SwiftUI 列表必需) - 遵循
Codable协议(支持持久化) - 使用值类型(
struct)确保数据不可变性
2. 模型操作方法
extension TodoItem {
mutating func toggleCompletion() {
isCompleted.toggle()
}
func isOverdue(now: Date = Date()) -> Bool {
guard let dueDate = dueDate else { return false }
return now > dueDate
}
}
3. 模型容器
class TodoStore: ObservableObject {
@Published var items: [TodoItem] = []
func addItem(_ title: String) {
let newItem = TodoItem(title: title)
items.append(newItem)
}
func removeItems(at indices: IndexSet) {
items.remove(atOffsets: indices)
}
}
完整实现示例
数据持久化扩展
extension TodoStore {
private static func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("todos.data")
}
func load() throws {
let url = try Self.fileURL()
let data = try Data(contentsOf: url)
items = try JSONDecoder().decode([TodoItem].self, from: data)
}
func save() throws {
let data = try JSONEncoder().encode(items)
let url = try Self.fileURL()
try data.write(to: url)
}
}
使用案例
// 创建实例
let store = TodoStore()
// 添加数据
store.addItem("学习SwiftUI")
store.addItem("完成MVVM项目")
// 修改数据
store.items[0].toggleCompletion()
// 持久化操作
do {
try store.save()
} catch {
print("保存失败: \(error)")
}
设计要点
- 不可变标识符:使用
UUID确保唯一性 - 可测试性:所有业务逻辑(如
isOverdue)不依赖外部状态 - 关注点分离:
- 模型只包含数据和基础操作
- 存储类处理集合操作和持久化
- SwiftUI 集成准备:
ObservableObject协议支持数据绑定@Published属性包装器自动触发视图更新
常见问题解决方案
问题1:如何处理日期显示?
extension TodoItem {
var dueDateString: String {
guard let date = dueDate else { return "无期限" }
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter.string(from: date)
}
}
问题2:如何实现排序功能?
extension TodoStore {
func sortByDueDate(ascending: Bool = true) {
items.sort {
switch ($0.dueDate, $1.dueDate) {
case (nil, nil): return false
case (nil, _): return false
case (_, nil): return true
case (let lhs?, let rhs?):
return ascending ? lhs < rhs : lhs > rhs
}
}
}
}
提示:在实际项目中,建议将日期格式化等显示逻辑放在 ViewModel 层,保持 Model 只关注核心数据。
