首先看下主要内容:

在本教程中,您将学习使用@State,@Environment和@FetchRequest属性包装器将数据持久保存在应用程序中。内容来自翻译。

接着看下写作环境:

Swift 5, iOS 13, Xcode 11

下面就是正文了

想象一下,记下Notes中的一些重要内容,却发现下次打开应用程序时数据消失了!幸运的是,持久化在iOS上非常出色。多亏了Core Data,所有笔记,照片和其他数据都是安全的。

需要跨应用程序启动存储数据时,可以使用多种不同的技术。Core Data是iOS上的首选解决方案。 Apple的Core Data框架具有出色的性能和广泛的功能,可管理应用程序的整个模型层,并处理对设备存储磁盘的持久性。

在本教程中,您将重构应用程序以增加持久化,并防止在应用程序重启时丢失数据的噩梦。在此过程中,您将学习:

在项目中设置Core Data。

使用SwiftUI的数据流访问Core Data框架中所需的内容。

使用Core Data定义和创建新的模型对象。

使用Fetch Requests从磁盘检索对象。

因此,下面一起来了解有关Core Data功能及其工作原理的更多信息!

打开起始项目,并build


欢迎使用FaveFlicks,您自己喜欢的电影的个人收藏。 这是一个简单的应用程序,可让您在列表中添加或删除电影。 但是,它有一个明显的问题。

是的,您猜对了:该应用程序不会保留数据! 这意味着,如果您将一些电影添加到列表中,然后重新启动应用程序,则您精心添加的电影将消失。

1. Testing FaveFlick’s Persistence

要从列表中删除电影,请向左滑动并点按Delete。


接下来,点击右上角的加号按钮以添加您的收藏夹之一。


你将会看到Add Movie页面


每个Movie对象仅存在于内存中。 它们没有存储在磁盘上,因此关闭应用程序会删除您的更改并恢复到我喜欢的电影的列表。

注意:如果您尝试第二次打开add movie页面,则什么也不会发生。 这是SwiftUI中的一个已知Apple bug。 解决方法是,您需要以某种方式更新UI以添加更多电影。 您可以下拉列表以更新UI,然后添加更多电影。

强制关闭应用程序以测试其持久化。 将应用置于前台,进入快速应用切换器fast app switcher。 为此,请从屏幕底部轻轻向上拖动。 如果您的设备有一个,请双击Home按钮以启用快速应用程序切换器。


现在,选择FaveFlicks并向上滑动以关闭该应用程序。 在home屏幕上,点击FaveFlicks再次将其打开。

请注意,您所做的更改已消失,并且默认影片已恢复。


现在该修复此问题。首先设置Core Data。

Setting Up Core Data

在开始设置持久性之前,您应该了解Core Data的活动部分,也称为Core Data stack。Core Data stack包括:

定义模型对象的managed object model,(也称为实体(entities))及其与其他实体的关系。将其视为您的数据库架构(database schema)。在FaveFlicks中,您将将Movie实体定义为FaveFlicks.xcdatamodeld中managed object model的一部分。您将使用NSManagedObjectModel类在代码中访问您的managed object model。

NSPersistentStoreCoordinator,用于管理实际的数据库(actual database)。

NSManagedObjectContext,它是一个内存暂存器,可让您创建,编辑,删除或检索实体。通常,在与Core Data进行交互时,您将使用managed object context。

有了这些,就可以开始了!

1. Adding the Core Data stack

尽管设置整个Core Data stack似乎很艰巨,但要感谢NSPersistentContainer,这很容易。它可以为您创建一切。打开SceneDelegate.swift并在import SwiftUI之后添加以下内容:

import CoreData

Core Data存在于其自己的框架中,因此您必须导入它才能使用它。

现在,在SceneDelegate的末尾添加以下内容:

// 1
lazy var persistentContainer: NSPersistentContainer = {
// 2
let container = NSPersistentContainer(name: "FaveFlicks")
// 3
container.loadPersistentStores { _, error in
// 4
if let error = error as NSError? {
// You should add your own error handling code here.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()

下面就是要做的内容:

1) 将一个名为persistentContainer的懒加载属性添加到您的SceneDelegate。首次引用该属性时,它将创建一个NSPersistentContainer。

2) 创建一个名为FaveFlicks的容器。如果您在Project navigator中查看应用程序的文件列表,则会看到一个名为FaveFlicks.xcdatamodeld的文件。该文件是您稍后将在其中设计Core Data model schema的位置。该文件的名称必须与容器的名称匹配。

3) 指示容器加载persistent store,这将简单地设置Core Data stack。

4) 如果发生错误,则会记录错误并终止该应用程序。在真实的应用程序中,您应该通过显示一个对话框指示该应用程序处于怪异状态并需要重新安装来处理此问题。此处的任何错误都应该很少发生,并且是由于开发人员的错误造成的,因此,在将您的应用提交到App Store之前,请务必先发现错误。

就这些。这就是设置Core Data stack所需的全部。不是很难,对吧?

您还需要一种将任何数据保存到磁盘的方法,因为Core Data不会自动处理该数据。仍在SceneDelegate.swift中,在类末尾添加以下方法:

func saveContext() {
// 1
let context = persistentContainer.viewContext
// 2
if context.hasChanges {
do {
// 3
try context.save()
} catch {
// 4
// The context couldn't be saved.
// You should add your own error handling here.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}

这将创建一个名为saveContext()的方法,该方法将执行以下操作:

1) 获取持久性容器(persistent container)的viewContext。 这是一个特殊的managed object context,仅在主线程上使用。 您将用它来保存所有未保存的数据。

2) 仅当有更改要保存时才保存。

3) 保存上下文。 此调用可能会引发错误,因此包含在try / catch中。

4) 发生错误时,系统会记录该错误并将该应用终止。 就像以前的方法一样,此处的任何错误都应仅在开发期间发生,但应以适当的方式在您的应用程序中进行处理,以防万一。

既然您已经设置了Core Data stack,并且可以保存更改,现在是时候将其连接到应用程序的其余部分了。

现在,在scene(_:willConnectTo:options :)中,将let contentView = MovieList()替换为以下内容:

let context = persistentContainer.viewContext
let contentView = MovieList().environment(\.managedObjectContext, context)

这只是获取与您先前使用的相同的viewContext并将其设置为MovieList SwiftUI视图上的环境变量。 该视图稍后将使用此视图从Core Data存储中添加和删除电影。

现在,将以下方法添加到SceneDelegate的末尾:

func sceneDidEnterBackground(_ scene: UIScene) {
saveContext()
}

这指示应用程序在后台运行时调用您先前添加的save方法。 这是将数据保存到磁盘的好时机。 稍后,您将看到如何更频繁地保存。

构建并运行以检查该应用程序是否仍然有效。

Creating the Data Model

现在该是该应用程序主要部分上的工作了。 在Xcode中,打开FaveFlicks.xcdatamodel。 现在它是空的,但是您将在下面声明Movie实体(entity)。 在这里定义数据模型的schema。 您将添加相关的实体(可以创建的对象类型),并定义关系(relationships)以指示实体的连接方式。

单击Add Entity。


Xcode在data model中创建一个新实体,默认情况下名为Entity。 双击名称并将其更改为Movie


接下来,单击Attributes下的+图标以添加新属性。 将其命名为title并将类型设置为String。


最后,再添加两个属性:一个名为String的genre,另一个为Date类型的releaseDate。 完成后,Movie实体的属性将与以下各项匹配:


1. Relationships and Fetched Properties

尽管FaveFlicks仅具有一个Movie实体,但是在具有较大数据模型的应用程序中,您可能会遇到关系和获取的属性。 关系(relationship)与任何数据库中的关系相同:它使您可以定义两个实体之间的关系。

但是,Fetched properties是更高级的Core Data主题。 您可以将其视为类似于弱单向关系的计算属性。 例如,如果FaveFlicks具有Cinema实体,则它可能具有currentShowingMovies的Fetched properties,该属性将获取电影院中当前的Movies。

Removing the Old Movie Struct

打开Movie.swift。 在本教程开始时,Movie结构是模型对象(model object)。 Core Data创建了自己的Movie类,因此您需要删除Movie.swift。 通过在“项目”导航器中右键单击Movie.swift并选择Delete来删除它。 在出现的对话框中,单击Move to Trash。

Build应用。 您会看到几个需要修复的错误,因为您刚刚删除了Movie。


注意:您需要保持准确,并在本节中删除旧的Movie结构的大量代码,因此请密切注意!

首先,打开MovieList.swift。 您会找到存储在简单movies数组中的movies列表。 在MovieList的顶部,将声明movies数组的行更改为空数组,如下所示:

@State var movies: [Movie] = []

@State属性包装器是SwiftUI数据流的重要组成部分。 声明此本地属性的类拥有它。 如果有任何更改movies的值,则拥有它的视图将触发UI的更新。

现在,删除makeMovieDefaults(),因为它已不再使用。

在addMovie(title:genre:releaseDate :)中,将创建movies并将其添加到movies数组。 删除其内容并将其保留为空白方法。 您将在后面的部分中使用它来创建Movie实体的新实例。

最后,删除deleteMovie(at :)的内容。 您稍后将用删除Core Data实体的代码替换它。

Using the New Movie Entity

现在,您已经在数据模型(data model)中创建了Movie实体,Xcode将自动生成它自己的Movie类,您将使用它来代替。 数据模型(data model)中的所有实体都是NSManagedObject的子类。 这是一个managed object,因为Core Data主要通过使用Managed Object Context来为您处理生命周期和持久性。

旧的Movie结构没有使用可选属性。 但是,所有NSManagedObject子类都为其属性使用可选属性。 这意味着您需要对使用Movie的文件进行一些更改。

1. Using an Entity’s Attributes in a View

现在,您将学习在视图中使用实体的属性(attributes)。 打开MovieRow.swift。 然后,将body属性替换为:

var body: some View {
VStack(alignment: .leading) {
// 1
movie.title.map(Text.init)
.font(.title)
HStack {
// 2
movie.genre.map(Text.init)
.font(.caption)
Spacer()
// 3
movie.releaseDate.map { Text(Self.releaseFormatter.string(from: $0)) }
.font(.caption)
}
}
}

视图的结构完全相同,但是您会注意到所有movie attributes都已映射到Views。

Core Data entity上的所有属性(attributes)都是可选的。 也就是说,title属性的类型为String?,referenceDate的类型为Date? 等等。 因此,现在您需要一种获取可选值的方法。

在ViewBuilder中,例如MovieRows的body属性,您无法添加控制流语句(如if let)。 每行应为View或nil。

如果attributes为non-nil,则上面标记为1、2和3的行是Text视图。 否则,它为nil。 这是在SwiftUI代码中处理可选内容的便捷方法。

最后,构建并运行。 您删除了旧的Movie结构,并将其替换为Core Data实体。 作为奖励,您现在拥有空视图,而不是电影列表。


如果您制作电影,则什么也不会发生。 接下来,您将解决此问题。

Using Environment to Access Managed Object Context

接下来,您将学习如何从managed object context访问对象。 返回MovieList.swift,在movies声明下添加以下行:

@Environment(\.managedObjectContext) var managedObjectContext

还记得您之前在MovieList上设置了managedObjectContext环境变量吗? 好吧,现在您声明它已经存在,因此可以访问它。

@Environment是SwiftUI数据流的另一个重要部分,可让您访问全局属性。 当您要将环境对象传递给视图时,可以在创建对象时将其传递给视图。

现在,将以下方法添加到MovieList.swift中:

func saveContext() {
do {
try managedObjectContext.save()
} catch {
print("Error saving managed object context: \(error)")
}
}

创建,更新或删除实体时,需要在managed object context(内存暂存器)中进行。 要将更改实际写入磁盘,必须保存上下文。 此方法将新的或更新的对象保存到持久性存储中。

接下来,找到addMovie(title:genre:releaseDate :)。 从删除旧的Movie以来,该方法仍然是空白的,因此将其替换为以下方法以创建新的Movie实体:

func addMovie(title: String, genre: String, releaseDate: Date) {
// 1
let newMovie = Movie(context: managedObjectContext)
// 2
newMovie.title = title
newMovie.genre = genre
newMovie.releaseDate = releaseDate
// 3
saveContext()
}

在这里,您:

1) 在managed object context中创建一个新的Movie。

2) 设置将Movie的所有属性作为参数传递到addMovie(title:genre:releaseDate :)中。

3) 保存managed object context。

Build并运行和创建新电影。 您会注意到一个空白列表。


那是因为您正在创建电影,但没有检索它们以显示在列表中。 在下一节中,您将对其进行修复,最后您将再次在该应用程序中观到movies。

Fetching Objects

现在,您将学习如何显示自己制作的电影。 您需要使用FetchRequest从持久性存储中获取它们。

在MovieList的顶部,删除声明movies数组的行。 用以下FetchRequest替换它:

// 1
@FetchRequest(
// 2
entity: Movie.entity(),
// 3
sortDescriptors: [
NSSortDescriptor(keyPath: \Movie.title, ascending: true)
]
// 4
) var movies: FetchedResults

当您需要从Core Data检索实体时,可以创建FetchRequest。在这里,您:

1) 使用@FetchRequest属性包装器(property wrapper)声明该属性,该包装器可让您直接在SwiftUI视图中使用结果。

2) 在属性包装器内,指定要获取Core Data的实体。这将获取Movie实体的实例。

3) 添加一个排序描述符(sort descriptors)数组,以确定结果的顺序。例如,您可以按流派(genre)对Movie进行排序,然后对具有相同流派的电影按标题进行排序。但是在这里,您只需按标题排序。

4) 最后,在属性包装器之后,声明类型为FetchedResults的movies属性。

1. Predicates

这将获取Core Data存储的所有Movie。但是,如果您需要过滤对象或仅检索一个特定实体怎么办?您还可以使用谓词(predicate)配置fetched request以限制结果,例如仅获取特定年份的电影或匹配特定流派的电影。为此,您可以在@FetchRequest属性包装的末尾添加谓词参数,如下所示:

predicate: NSPredicate(format: "genre contains 'Action'")

您的提取请求应该会提取所有电影,因此现在无需添加它。 但是,如果您想尝试一下,那就一定要做!

Testing the Results

Build并运行。 您会看到电影列表。 恭喜你!


好吧,这只是使您回到起点。 要测试电影是否已存储到磁盘,请添加一些电影,然后按Xcode中的stop以终止该应用程序。 然后构建并再次运行。 您所有的电影仍将在那里!

Deleting Objects

接下来,您将学习删除对象。 如果向左滑动并尝试删除电影,则什么也不会发生。 要解决此问题,请将deleteMovie(at :)替换为:

func deleteMovie(at offsets: IndexSet) {
// 1
offsets.forEach { index in
// 2
let movie = self.movies[index]
// 3
self.managedObjectContext.delete(movie)
}
// 4
saveContext()
}

这是正在做的事情:

1) 滑动以删除列表中的对象时,SwiftUI List会为您提供删除的IndexSet。 使用forEach遍历IndexSet。

2) 获取当前index的电影。

3) 从managed object context中删除影片。

4) 保存上下文以将更改持久保存到磁盘。

构建并运行。 然后,删除电影。


大功告成!

在本教程中,您已经了解了很多数据流,但是如果您想了解更多信息,请观看WWDC 2019的数据流通过SwiftUIData Flow Through SwiftUI视频。