项目档案

SwiftUI 核心数据示例

我们的下一个例子是一个SwiftUI赢博体育,它利用核心数据框架在本地存储和管理一组更复杂的对象。示例赢博体育程序是一个简单的成绩簿赢博体育程序,允许用户记录学生的成绩以及一系列作业和考试。

该赢博体育程序有一个选项卡界面,用户可以通过选项卡创建和管理学生列表,创建和管理一组测验,还有一个选项卡,用户可以记录测验的成绩。

使用核心数据来管理赢博体育程序的数据

我们的示例赢博体育程序需要能够存储和检索相关对象的集合。这个赢博体育程序将处理学生对象、测验对象和年级对象。此外,这些事情是相互关联的:每个学生都连接到一组成绩,每个测验都连接到一组成绩。

核心数据框架是一个用于管理和存储赢博体育程序中相关对象网络的系统。这个例子将向你展示如何在SwiftUI赢博体育程序中使用核心数据。

当你在Xcode中创建一个新的赢博体育程序时,你会在设置项目时看到一个复选框,你可以勾选它来在你的项目中包含Data。选中此选项会自动为您的项目生成大量代码和其他资源,以便您开始使用核心数据。对于核心数据中的第一个示例,自动生成的代码将会有点压倒性,所以在设置这个示例项目时,我选择在设置项目时不检查该选项。相反,我将引导您手动设置和使用核心数据的过程。这样我们就可以在项目中包含足够有用的核心数据。

创建一组实体

核心数据为我们管理的类叫做实体。我们将使用核心数据来设置这些类。设置实体的第一步是将数据模型添加到项目中。要创建数据模型,右键单击Xcode中的项目文件夹并选择新文件选项。在新建文件对话框中选择核心数据/数据模型选项。

数据模型跟踪项目中的赢博体育实体及其属性和关系。当数据模型视图打开时,你会看到一个Core data用来设置赢博体育这些东西的特殊用户界面。

要在数据模型中创建一个新实体,首先单击Add entity按钮。我们为实体提供一个名称,然后为实体提供一个或多个属性。我们通过单击窗口Attributes部分中的+按钮来添加属性。每个属性都有一个名称和类型。当我们在核心数据中创建实体时,系统将自动为我们创建的每个实体生成一个类。那个类将拥有与我们赋予实体的属性相对应的属性。

当我们在赢博体育中创建实体对象时核心数据会将这些对象存储在它为我们生成的数据库中。具体来说,该数据库是一个SQLite数据库。对于我们在核心数据中设置的每个实体类型,数据库中将有一个相应的表,其中的列与实体的属性相对应。

在设置了Student、Quiz和Grade实体之后,我们还需要描述这些类之间的连接。我们通过向数据模型中添加关系来实现这一点。要创建新关系,单击关系中涉及的一个实体,然后单击数据模型的Relationships部分中的+按钮。关系将一个实体连接到另一个实体:关系来自的实体是源,而关系连接到的实体是目的。可以通过在关系的属性检查器中设置destination属性来选择目标。您需要注意的另一个属性是Type属性。这里的选项是“To One”或“To Many”。在从实体a到实体B的关系中,每个a对象既可以连接到单个B对象,也可以连接到B对象列表。例如,在我们的赢博体育程序中,每个Student对象将连接到许多Grade对象,每个Quiz对象将连接到许多Grade对象。最后,我们还要指定这是否是一个逆关系。如果关系是从实体A到实体B的逆关系,则每个B对象将包含一个属性,该属性允许我们返回到它所连接的A对象。对于我们的赢博体育,我们希望两个关系都有逆。这样,每个Grade实体都将拥有一个quiz属性,将其链接回该分数所针对的测验,并拥有一个student属性,将该分数链接到该分数所在的学生。

为核心数据添加代码

在赢博体育中设置核心数据的下一步是设置一个PersistentContainer对象。这个对象将是我们通往核心数据系统的门户。赢博体育加载和存储实体的请求都将通过这个对象。设置PersistentContainer的一种方便方法是将其包装在一个简单的模型类中。我为此目的使用的模型类是一个数据控制器:

让容器= NSPersistentContainer(name: "GradeBook") init(){容器。loadPersistentStores {description,如果让err = error {print("核心数据 failed to load \(error . localizeddescription)")}}}}

为了使这个类的容器属性在整个赢博体育程序中可用,我在赢博体育程序的app对象中设置了一个DataController,并将其容器作为一个环境对象共享:

@主结构UIGradesApp: App {@ statobject私有var dataController = dataController () var body: some Scene {WindowGroup {MainView().environment(\.managedObjectContext,dataController.container. viewcontext)}}

MainView类

因为我们的赢博体育程序的界面是一个TabView,里面有三个视图,我们的赢博体育程序的第一个视图类将设置TabView:

struct MainView: View {@Environment(\. managedobjectcontext) var moc 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(\. managedobjectcontext) var moc @FetchRequest(sortDescriptors: [SortDescriptor(\.last_name),SortDescriptor(\.first_name)]) var students: FetchedResults<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 ? ?“未知”)\(学生。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(context: moc) Student。id = UUID() student。Last_name = Last_name student。First_name = First_name try?moc.save()}}} func delete(at offset: IndexSet) {offset .save()}}forEach {i in moc.delete(students[i]) try?Moc.save ()}}

你将在这里看到的一种新属性是核心数据 @FetchRequest属性。在核心数据中,FetchRequest本质上是一个数据库查询,它从数据库中提取一组对象,还包括如何在对象从数据库中取出时对其排序的信息。在这个视图中,我们正在与学生一起工作,所以我们希望赢博体育程序中赢博体育学生对象的列表首先按姓氏排序,然后按名字排序。

默认情况下,获取请求将加载数据库中给定类型的赢博体育对象。您可以通过在获取请求中提供谓词来获取这些对象的一个子集。我们将在后面的注释中看到一个这样的例子。

该视图将显示当前系统中赢博体育Student对象的列表,以及一些允许用户创建新学生的附加元素。“Add Student”按钮的动作闭包演示了如何使用核心数据创建一个新实体。我们使用核心数据自动为我们生成的Student类在上下文中创建一个新的Student对象,设置其属性,然后告诉上下文将更改保存到数据库中。

这个视图中还有代码演示如何删除核心数据对象。我向学生列表添加了一个onDelete()修饰符,该修饰符调用视图的delete()方法来处理删除。幸运的是,我们的核心数据上下文提供了一个delete()方法,我们可以将一个对象传递给delete。要删除的对象位于链接到原始获取请求的数组中。

第二个视图管理测验列表,非常类似。

管理成绩

一旦我们有一组学生和测验设置在我们的赢博体育程序,我们将能够开始记录成绩。这个视图是这样的:

视图的顶部是两个Picker对象,允许用户从可用学生列表中选择Student,从可用测验对象列表中选择Quiz。在选择器下面,我们有一个文本字段,用户可以在其中输入该学生在测试中的成绩,还有一个按钮用于将成绩保存到数据库中。请注意,在某些情况下,我们选择的学生/测验组合将已经有一个成绩:在这种情况下,我们只需要更新我们之前记录的成绩。

下面是GradeView的代码:

struct GradeView: View {@Environment(\.managedObjectContext) var moc @FetchRequest(sortDescriptors: []) var quizzes: FetchedResults<Quiz> @FetchRequest(sortDescriptors: [SortDescriptor(\.last_name),SortDescriptor(\.first_name)]) var students: FetchedResults<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。}} Picker(Select: $student,label:Text("Select a student:“)) {ForEach(students) {s in Text(”first_name ? ?“未知”)\ (s。last_name ? ?“未知”)”).tag (s。}} Spacer()如果让q = quiz,让s = Student{让g = getGrade(quiz: q, Student: s) GradeDetailView(grade: g, Student: s,quiz: q)} Spacer()}。onAppear {quiz = quizzes。第一个学生=学生。第一个}}}函数getGrade(quiz q: quiz,student s: student) -> Grade!{让sPred = nspredate(格式:"学生。Id == %@", s.d Id !作为NSUUID)让qPred = NSPredicate(格式:“测验。Id == %@", q.id!作为NSUUID) let request = Grade.fetchRequest()请求。谓词= NSCompoundPredicate(andPredicateWithSubpredicates: [sPred,qPred])请求。sortDescriptors = [] var g:分数!= nil do{让成绩=尝试moc.fetch(请求)如果成绩。Count > 0 {g = grades[0]}} catch {} return g}}

与前面两个视图一样,有@FetchRequest属性来加载赢博体育Student和Quiz对象的列表。然后我们使用Picker类来制作显示这些列表的Picker。当我们设置一个Picker时,我们必须为这个Picker提供一个选择属性,这是一个绑定到一个状态变量的属性,该状态变量将存储用户从这个Picker中所做的选择。当我们使用ForEach构造设置选择器中显示的项时,我们还希望为每个显示的选项提供一个与选择项具有相同类型的标记属性。当用户选择一个选择器项时,选择器会将该项的标记复制到选择属性中。

一旦用户从第一个选择器中选择了一个测验,从第二个选择器中选择了一个学生,我们将想要查看是否已经为该组合创建了Grade对象。获取Grade对象所需的逻辑封装在getGrade()方法中。这里的代码演示了在核心数据中运行fetch请求的另一种方式。由于我们正在尝试获取Grade对象,因此我们首先要求Grade类给我们一个获取请求对象。然后用谓词和排序描述符提供取回请求,然后将取回请求传递给核心数据上下文来执行取回。

从上面的代码中可以看到,设置谓词来过滤结果首先要创建NSPredicate对象。每个NSPredicate对象都包含一个格式字符串,它看起来有点像SQL预处理语句。格式字符串包含一个或多个@占位符来填充。这些占位符的值由格式字符串后面的参数提供。

对于这个特定的查询,我们希望找到一个与学生/测验组合相链接的Grade对象。为此,我设置了两个谓词。第一个谓词将获取数据库中赢博体育的Grade对象,这些对象的student属性的id号与我们被告知要查找的学生的id号匹配。这将有效地为该学生争取赢博体育的成绩。同样,第二个谓词将加载我们感兴趣的测验的赢博体育Grades。最后,我们用于获取请求的实际谓词是一个复合谓词,由两个更简单谓词的和组成。这将最终只获取系统中该学生和测验的一个Grade。由于数据库中可能没有该组合的Grade,因此我们安排getGrade()方法返回一个可选的Grade。如果当前没有记录成绩,该方法将返回nil。

一旦我们找到了我们想要的Grade,我们将该Grade传递给GradeDetailView类,该类将允许用户更新现有的Grade或添加新的Grade。下面是这个视图类的代码:

struct GradeDetailView: View {@Environment(\.managedObjectContext) var moc让grade: grade !让学生:学生!让我们测验一下:测验一下!@状态var分数:字符串init(等级g:等级!)学生5:学生!,quiz q: quiz !) {grade = g student = s quiz = q if let gr = grade {_score = State(initialValue: String(gr.points))} else {_score = State(initialValue: "")}} var body: some View {VStack {HStack {Text("Score:") TextField("", Text:$ Score)} Button("Save") {if let g = grade {g.points = Int32(Score) ??0} else {let g = Grade(context: moc) g.id = UUID() g.quiz = quiz g.student = student g.points = Int32(score) ??0} try?mod .save()}}}}

该视图包含一个状态变量,该变量链接到用户可以在其中输入等级的文本字段。如果有的话,我们希望用现有的grade预填充文本字段,因此我在这里添加了一个自定义init()方法,该方法将确定是否存在现有的grade。如果有,我们将把Grade的points属性复制到score属性中。这里的代码显示了在init()方法中初始化状态变量所需的特殊过程。

在链接到“Save”按钮的闭包中,你可以看到用于更新现有Grade的points属性或创建新Grade对象的代码。在进行任何更改之后,我们调用上下文的save()方法将更改提交到数据库。