版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.05.30 星期六 |
前言
数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说
plist
文件(属性列表)、preference
(偏好设置)、NSKeyedArchiver
(归档)、SQLite 3
、CoreData
,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
3. 数据持久化方案解析(三) —— 基于NSCoding的持久化存储(一)
4. 数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二)
5. 数据持久化方案解析(五) —— 基于Realm的持久化存储(一)
6. 数据持久化方案解析(六) —— 基于Realm的持久化存储(二)
7. 数据持久化方案解析(七) —— 基于Realm的持久化存储(三)
8. 数据持久化方案解析(八) —— UIDocument的数据存储(一)
9. 数据持久化方案解析(九) —— UIDocument的数据存储(二)
10. 数据持久化方案解析(十) —— UIDocument的数据存储(三)
开始
首先看下主要内容:
在本教程中,您将学习使用
@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视频。
后记
本篇主要讲述了基于Core Data 和 SwiftUI的数据存储示例,感兴趣的给个赞或者关注~~~