我们的下一个例子是一个SwiftUI赢博体育程序,它利用SwiftData框架在本地存储和管理一组更复杂的对象。示例赢博体育程序是一个简单的成绩簿赢博体育程序,允许用户记录学生的成绩以及一系列作业和考试。
该赢博体育程序有一个选项卡界面,用户可以通过选项卡创建和管理学生列表,创建和管理一组测验,还有一个选项卡,用户可以记录测验的成绩。
我们的示例赢博体育程序需要能够存储和检索相关对象的集合。这个赢博体育程序将处理学生对象、测验对象和年级对象。此外,这些事情是相互关联的:每个学生都连接到一组成绩,每个测验都连接到一组成绩。
SwiftData框架是一个用于管理和存储赢博体育程序中相互关联的对象网络的系统。本例将向您展示如何在SwiftUI赢博体育程序中使用SwiftData。
SwiftData为我们管理的类被称为模型类。这些基本上是普通的Swift类,它们添加了一些SwiftData特定的注释。
下面是我们将在这个赢博体育程序中使用的Model类的代码:
@Model final class Student {@Attribute(.unique) var id: String = UUID()。uuidString var first_name:字符串var last_name:字符串var grades: [Grade] init(id: String = UUID())。uuidString,名字:字符串,姓氏:字符串){self。Id = Id self。First_name = firstname。Last_name = lastname。@Model final class Quiz {@Attribute(.unique) var id: String = UUID()。uuidString var name:字符串var points: Int var grades: [Grade] init(id: String = UUID())。uuidString, name: String, points: Int) {self。Id = Id self.name = name self.name点=点本身。@Model final class Grade {@Attribute(.unique) var id: String = UUID()。uidstring var score: Int var student: student ?var quiz: quiz ?init(id: String = UUID())。uuidString,得分:Int){自我。Id = Id self。分数=分数}}
SwiftData对Model类有两个特定的要求。首先,必须用@Model对它们进行注释。其次,每个类都需要有一个存储唯一标识符的属性。在赢博体育这三个类中,我都添加了一个带有@Attribute注释的id属性来满足这一要求。
关于这些类需要注意的最后一点与类之间的关系有关。数据库实体之间的关系是一个需要考虑的重要细节:幸运的是,SwiftData可以很容易地以一种非常自然的、面向对象的方式捕获这些关系。这里的关键关系是一对多关系,它表示一个学生有多个分数,而一对多关系表示一个测验将分配多个分数。这些关系在类声明中以一种非常自然的方式被捕获。SwiftData足够强大,可以将这些面向对象的关系转换为数据库中的关系。
在赢博体育中设置SwiftData的下一步是设置一个ModelContainer对象。这个对象将是我们到SwiftData系统的网关。当我们创建或销毁模型对象时,我们需要从ModelContainer中插入或删除这些对象。
设置一个ModelContainer的过程通常是从赢博体育程序的Application对象开始的。下面是该类的代码:
导入SwiftData @main struct SwiftDataGradesApp: App {var sharedModelContainer: ModelContainer = {let schema = schema ([Student.self,Quiz.self,Grade. self])。let modelConfiguration = modelConfiguration (schema: schema, isStoredInMemoryOnly: false) do {return try ModelContainer(for: schema, configurations: [modelConfiguration])} catch {fatalError("Could not create ModelContainer: \(error)")}}() var body: some Scene {WindowGroup {ContentView()} .modelContainer(sharedModelContainer)}}
这里的代码创建了Schema对象和ModelConfiguration对象。然后将这些信息传递给ModelContainer。最后,我们需要安排ModelContainer对赢博体育程序的组件可用。标准的方法是将ModelContainer作为环境对象向下传递。modelcontainer()修饰符函数是一个方便的函数,旨在简化这一操作。
因为我们的赢博体育程序的界面是一个TabView,里面有三个视图,我们的赢博体育程序的第一个视图类将设置TabView:
import SwiftUI import SwiftData struct ContentView: View {var body: some View {TabView {StudentView()。tabItem {Label("Students",systemImage: "person.fill")} QuizView()tabItem {Label("Quizzes",systemImage: "question .folder")} GradeView()。tabItem {Label("Grades",systemImage: "percent")}}}}
“更多组件”示例介绍了如何设置选项卡视图。
前两个视图用于设置Students和Quiz对象。下面是StudentView类的代码:
struct StudentView: View {@Environment(\.modelContext) private var modelContext @Query private var students: [Student] @State var first_name: String = "" @State var last_name: String = "" var body: some View {VStack {List {ForEach(students) {Student in Text("\(Student .first_name) \(Student .last_name)")}。onDelete(perform: delete)} HStack {Text("First Name:") TextField("", Text:$first_name).disableAutocorrection(true).textFieldStyle(.roundedBorder)} HStack {Text("Last Name:") TextField("", Text:$last_name).disableAutocorrection(true).textFieldStyle(.roundedBorder)} Button("Add Student") {let Student = Student(firstname: first_name, lastname: last_name) modelContext.insert(Student) try?modelContext.save()}}} func delete(at offsets: IndexSet){偏移量。forEach {i in modelContext.delete(students[i])} try?modelContext.save()}}
注意带有@Environment注释的属性。这使我们能够访问传递给我们一个环境对象的ModelContainer。
你将在这里看到的一种新属性是SwiftData @Query属性。在SwiftData中,Query属性本质上是一个数据库查询,它从数据库中提取一组对象。在这个视图中,我们与学生一起工作,所以我们需要赢博体育程序中赢博体育学生对象的列表。
默认情况下,查询请求将加载数据库中给定类型的赢博体育对象。您可以通过在查询请求中提供谓词来获取这些对象的一个子集。我们将在后面的注释中看到一个这样的例子。
该视图将显示当前系统中赢博体育Student对象的列表,以及一些允许用户创建新学生的附加元素。‘Add Student’按钮的动作闭包演示了如何使用SwiftData创建一个新实体。我们创建一个新的Student对象,然后告诉ModelContainer插入新对象,然后将更改保存到数据库中。
这个视图中还有代码演示如何删除SwiftData对象。我向学生列表添加了一个onDelete()修饰符,该修饰符调用视图的delete()方法来处理删除。幸运的是,ModelContainer提供了一个delete()方法,我们可以通过该方法传递一个对象来进行删除。要删除的对象位于链接到原始获取请求的数组中。
第二个视图管理测验列表,非常类似。
一旦我们有一组学生和测验设置在我们的赢博体育程序,我们将能够开始记录成绩。这个视图是这样的:
视图的顶部是两个Picker对象,允许用户从可用学生列表中选择Student,从可用测验对象列表中选择Quiz。在选择器下面,我们有一个文本字段,用户可以在其中输入该学生在测试中的成绩,还有一个按钮用于将成绩保存到数据库中。请注意,在某些情况下,我们选择的学生/测验组合将已经有一个成绩:在这种情况下,我们只需要更新我们之前记录的成绩。
下面是GradeView的代码:
struct GradeView: View {@Environment(\.modelContext) private var modelContext @Query private var quizzes: [Quiz] @Query private var students: [Student] @State var Quiz: Quiz?@State var student:学生?var body:一些视图{如果测验。count == 0 {Text("No quizzes available to display")} else if students。count == 0 {Text(“没有学生可显示”)}else {VStack {Picker(selection: $quiz,label:Text("Select a quiz:")) {ForEach(quizzes) {q in Text(q.name).tag(q.self as quiz ?)}} Picker(selection: $student,label:Text("Select a student:")) {ForEach(students) {s in Text("\(s.first_name) \(s.last_name)").tag(s. last_name)。如果让q = quiz,让s = Student {GradeDetailView(Student: s,quiz: q)} Spacer()}。onAppear {quiz = quizzes。第一个学生=学生。首先}}}}
与前面两个视图一样,有@Query属性来加载赢博体育Student和Quiz对象的列表。然后我们使用Picker类来制作显示这些列表的Picker。当我们设置一个Picker时,我们必须为这个Picker提供一个选择属性,这是一个绑定到一个状态变量的属性,该状态变量将存储用户从这个Picker中所做的选择。当我们使用ForEach构造设置选择器中显示的项时,我们还希望为每个显示的选项提供一个与选择项具有相同类型的标记属性。当用户选择一个选择器项时,选择器会将该项的标记复制到选择属性中。
一旦用户从第一个选择器中选择了一个测验,从第二个选择器中选择了一个学生,我们将希望查看是否已经为该组合创建了Grade对象,并为用户提供记录新成绩或更新现有成绩的机会。完成赢博体育这些操作所需的逻辑被封装在一个特殊的GradeDetailView组件中。
下面是该组件的代码:
struct GradeDetailView: View {@Environment(\.modelContext) private var modelContext让student: student让quiz: quiz @State var score: String = "" @Query var grades: [Grade] init(student: student,quiz: quiz) {self. View: View}学生=学生自己。让sId =学生。id let qId = quiz。id _grades =查询(filter: #Predicate {grade in (grade.student?)id == sId) && (grade.quiz?)id == qId)})} var body:一些View {VStack {if等级。isEmpty {HStack {Text("Score:") TextField("", Text:$ Score)} Button("Save") {let g = Grade(Score: Int(Score) ??0) g.quiz = quiz g.student = student. quiz.grades.append(g) student.grades.append(g) try?modelContext.save()}} else {Text("Existing score is \(grades[0].score)") HStack {Text("Update score:") TextField("", Text:$score)} Button("Save") {grades[0].score。分数= Int(分数)??0试试吗?modelContext.save()}}}}}
当视图加载时,我们将查看该学生和该测验是否有现有的成绩。如果是,我们将在视图中显示现有的分数。为了设置对现有的搜索,我在这里添加了一个自定义init()方法。在init()方法中发生的最重要的事情是这一行:
_grades =查询(filter: #Predicate {grade in .
(grade.student ?。id == sId) && (grade.quiz?)id == qId)
这行代码设置了一个动态查询,用于搜索指向该学生和该测验的一个Grade对象。Query中的filter参数提供了一个谓词,该谓词是一个lambda表达式,将赢博体育于数据库中的每个Grade。查询结果列表中只有那些在谓词中求值为true的Grades才会返回。
在body属性中,您将看到一些逻辑分为两种情况:要么已经存在Grade,要么我们需要创建一个新的Grade。
在链接到“Save”按钮的闭包中,您可以看到用于更新现有等级的points属性或创建新等级对象的代码。在进行任何更改之后,我们调用上下文的save()方法将更改提交到数据库。