1.最终效果
截屏2025-02-28 14.42.09.png
截屏2025-02-28 14.42.16.png
截屏2025-02-28 14.42.20.png
2.数据方面
数据方面包括:数据模型(使用结构体表示)、数据持久化、数据管理
2.1数据模型
/ 定义数据模型
//struct ListItem: Identifiable, Codable {
// var id = UUID()
// let title: String
// let content: String
// let date: Date
//}
struct ListItem: Identifiable, Codable {
let id: UUID // 移除默认值
let title: String
let content: String
let date: Date
// 双用途初始化器
init(
id: UUID = UUID(), // 默认生成新ID
title: String,
content: String,
date: Date = Date() // 默认使用当前时间
) {
self.id = id
self.title = title
self.content = content
self.date = date
}
}
使用带有初始值的不可变属性(let id = UUID())在解码时不会被覆盖,因为初始化后的值无法改变。这意味着当从JSON或其他数据格式解码时,id会被初始化为默认的UUID(),而不是解码出来的值。
Codable需要能够编码和解码所有存储属性。当使用let声明id并赋予初始值时,解码器无法修改这个属性,因为它是一个常量。因此,当用户尝试从持久化存储(如UserDefaults)加载数据时,id实际上会被重新生成,而不是恢复之前保存的值,这会导致数据不一致的问题。
使用let,这是基于Identifiable协议和SwiftUI的要求,需要id稳定不变。但这里的问题在于Codable的解码过程需要能够设置id的值,而使用let阻止了这一点,导致冲突。
结构体在遵循Codable协议时,编译器会自动生成一个解码初始化器,但如果存在常量属性(let)并且有默认值,编译器可能无法正确合成初始化器。因此,需要手动实现解码的初始化器,以便在初始化时为id赋值。
init中给出类型和等于一个函数的区别是一个是必填,一个是可选。
🧩 参数默认值设计差异解析
在 ListItem
的初始化器中,id
和 title
参数的设计差异源于它们的业务属性和数据特性:
init(
id: UUID = UUID(), // 自动生成唯一标识
title: String, // 必须显式提供
content: String, // 必须显式提供
date: Date = Date() // 自动记录当前时间
)
📌 核心差异对照表
参数 | 默认值 | 必需性 | 数据来源 | 设计意图 |
---|---|---|---|---|
id |
✅ UUID() | 可选 | 系统自动生成 | 保证唯一性,避免人工干预 |
title |
❌ 无 | 必填 | 用户输入 | 强制有效数据输入,避免空内容 |
date |
✅ Date() | 可选 | 系统自动获取 | 准确记录创建时间,简化用户操作 |
content |
❌ 无 | 必填 | 用户输入 | 确保内容完整性,符合业务逻辑要求 |
// 数据管理类
class ListData: ObservableObject {
@Published var items: [ListItem] = []
init() {
loadItems()
}
// 从UserDefaults加载数据
func loadItems() {
if let data = UserDefaults.standard.data(forKey: "items") {
if let decoded = try? JSONDecoder().decode([ListItem].self, from: data) {
items = decoded
}
}
}
// 保存数据到UserDefaults
func saveItems() {
if let encoded = try? JSONEncoder().encode(items) {
UserDefaults.standard.set(encoded, forKey: "items")
}
}
// 添加新项目
func addItem(title: String, content: String) {
let newItem = ListItem(title: title, content: content, date: Date())
items.insert(newItem, at: 0)
saveItems()
}
// 删除项目
func deleteItem(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
saveItems()
}
}
这里面UserDefaults.standard是轻量级数据存储的类。
UserDefaults.standard.data(forKey: "items")里的.data指获取的是Data类型数据,即二进制数据,
let user = User(name: "Alice", age: 30)
if let data = try? JSONEncoder().encode(user) {
UserDefaults.standard.set(data, forKey: "user")
}
这个Data数据是通过JSONEncoder处理过的二进制数据,上面是处理例子。
3.视图方面
// 主界面
struct ContentView: View {
@StateObject var listData = ListData()
@State private var showingAddView = false
var body: some View {
NavigationStack {
List {
ForEach(listData.items) { item in
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(item.title)
.font(.headline)
Spacer()
Text(item.date, style: .date)
.font(.caption)
.foregroundColor(.gray)
}
Text(item.content)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
.padding(.vertical, 8)
}
.onDelete(perform: listData.deleteItem)
}
.navigationTitle("事项列表")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddView = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddView) {
AddItemView(listData: listData)
}
}
}
}
// 添加新项目界面
struct AddItemView: View {
@ObservedObject var listData: ListData
@Environment(\.dismiss) var dismiss
@State private var title = ""
@State private var content = ""
var body: some View {
NavigationStack {
Form {
Section(header: Text("基本信息")) {
TextField("标题", text: $title)
TextField("内容", text: $content)
}
}
.navigationTitle("新建事项")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("保存") {
listData.addItem(title: title, content: content)
dismiss()
}
.disabled(title.isEmpty || content.isEmpty)
}
}
}
}
}
// 预览
#Preview {
ContentView()
}