CoreData
- 如何在SwiftUI中使用CoreData
- [在SwiftUI中如何配置Core Data](#在SwiftUI中如何配置Core Data "#View2")
- 如何从SwiftUI视图访问核心数据管理对象Context
- 如何使用@FetchRequest创建一个核心数据获取请求
- 如何使用predicate过滤CoreData的fetchRequests
- 如何从SwiftUI视图添加CoreData对象
- 如何从SwiftUI视图中删除CoreData对象
- 如何限制获取FetchRequest中的item数量
- 如何在SwiftUI中使用NSUserActivity
概述
文章主要分享SwiftUI Modifier的学习过程,将使用案例的方式进行说明。内容浅显易懂,CoreData部分调试,不过测试代码是齐全的。如果想要运行结果,可以移步Github下载code -> github案例链接
1、如何在SwiftUI中使用CoreData
SwiftUI和CoreData
作为Apple软件平台的其中两个重要组成部分,有很好的协同能力,SwiftUI有属性包装器、环境支持等等,这些都是为了确保能以最小的麻烦将Core Data集成到SwiftUI的App中。在SwiftUI之前,在架构角度来看,使用Core Data的方式如下:
- Apple建议在AppDelegate的级别创建容器,然后根据需要return
- 一部分人喜欢使用Manager Classes
- 还有一部分人喜欢完全抽象Core Data,以方便完全转移到Realm或其他可选项
SwiftUI与Core Data的集成是不同的,在启动时,需要创建Core Data容器,然后将其托管对象注入到环境中,然后直接在那里执行Request。以下是四个特定的功能,可以帮你理解我的意思:
- NSManagedObject遵守ObservableObject协议,这代表可以将任何对象绑定到用户界面。
- 环境中有一个
managedObjectContent
的key,用于存储Core Data托管对象的上下文。 - 然后Xcode的模板将这个context注入到初始的内容视图中。
- 有一个
@FetchRequest
的属性包装器,它使用环境的托管对象上下文来获取request
因此,在应用启动时创建一个托管对象上下文,将其附加到视图的环境中,然后使用@FetchRequest加载数据供App使用。
2、在SwiftUI中如何配置Core Data
如果创建一个新项目,使用的是SwiftUI并需要使用Core Data,Xcode的配置分为四步:
- 创建一个空的项目模板,通常以你的项目命名(FFModifier.xcdatamodeld),和一个示例配置
- 添加一个Persistence.swift文件,将Core Data整齐的封装在这个地方。
- 使用managedObjectContext将上下文注入初始ContentView的environment中。
- 创建在ContentView中创建、读取和删除示例数据的示例代码。
这是提供了在SwiftUI中使用CoreData获取request的完整能力。
2.1、创建Main.xcdatamodeld
通过cmd+N来创建一个新文件,然后选择Data Model来创建一个Core Data模型。这个模型的名字很重要,因为要在代码中引用,我这里命名为"Main"。一旦创建了model,那么就可以在任何想要使用的位置创建实例。
2.2、创建ProgrammingLanguage的实例
打开xcdatamodeld文件并创建一个名为ProgrammingLanguage
的实例,我创建了两个字符串属性:"name"和"creator"。你可以根据你的需求创建想要的任何属性了。
2.3、创建PersistenceController单例
需要一个地方来加载和管理Core Data配置,Apple的模版通过一个PersistenceController
单例实现。主要做的工作是让Core Data启动并运行,同时也为SwiftUI提供与来Contenxts的能力。
Swift
import CoreData
struct PersistenceController {
//创建单例
static let shared = PersistenceController()
//Core Data
let container: NSPersistentContainer
//SwiftUI预览配置
static var preview: PersistenceController = {
let controller = PersistenceController(inMemory: true)
//创建10个example
for i in 0..<100 {
let language = ProgrammingLanguage(context: controller.container.viewContext)
language.name = "Example Language \(i)"
language.creator = "A.Programmer \(i)"
}
return controller
}()
//加载Core Data初始化器,将使用内存存储设定为可选值
init(inMemory: Bool = false) {
//将Main更改为你自己的名
container = NSPersistentContainer(name: "Main")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(filePath: "/dev/null")
}
container.loadPersistentStores { descroption, error in
if let error = error {
fatalError("BBLv Error: \(error.localizedDescription)")
}
}
}
//向PersistenceController类添加save方法,以便它检查context是否更改。
func save() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
print("save error: \(error.localizedDescription)")
}
}
}
}
2.4、将Core Data容器的托管对象context注入到SwiftUI环境中
Swift
let persistenceController = PersistenceController.shared
2.5、scenePhase监听App状态
最后一步是可选的,但建议使用,来确保数据完整,针对的场景是当App状态切换为后台时,应该调用save()
方法,以便Core Data来保存你的更改,在SwiftUI中,通过添加一个属性到App来监听场景。
Swift
@Environment(\.scenePhase) var scenePhase
2.6、结合起来的代码
这段代码写在FFModifierApp中,APP的main入口,由于做了很多其他的例子也用到了这个入口,目前这里代码比较多,这是我整理的只有关于CoreData部分的代码。
Swift
@main
struct FFModifierApp: App {
//将Core Data容器的托管对象context注入到SwiftUI环境中,这里使用perview使用的是假数据,
//如果真实情况下,使用单例进行初始化(PersistenceController.shared)
let persistenceController = PersistenceController.preview
//最后一步是可选的,但建议使用,来确保数据完整,针对的场景是当App状态切换为后台时,
//应该调用save()方法,以便Core Data来保存你的更改,在SwiftUI中,通过添加一个属性到App来监听场景。
@Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
FFCoreDataFetchRequest()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
.onChange(of: scenePhase) {
persistenceController.save()
}
}
}
在上面的PersistenceController.swift类中我选择将数据保存在内存中了,所以该代码的内存存储部分很重要,因为Core Data配置为将信息保存到内存而不是磁盘是,这代表着Core Data中的数据被定义为临时数据,在程序关闭时会被清理。
3、如何从SwiftUI视图访问核心数据管理对象Context
核心代码为@Environment(\.managedObjectContext) var managedObjectContext
,在每一个想要使用CoreData的上下文内容上都要使用这个环境变量。下面的例子仅代表着FFCoreDataManagged想要使用数据。但是啥也没干。
Swift
struct FFCoreDataManagged: View {
//将context作为环境变量传递给ContentView(我这里是FFCoreDataManagged),
//在此视图中添加一个@Environment属性来读取托管对象上下文。
@Environment(\.managedObjectContext) var managedObjectContext
var body: some View {
Text("Hello, World!")
}
}
4、如何使用@FetchRequest创建一个核心数据获取请求
FetchRequest可以加载符合指定的条件的Core Data结果,并且SwiftUI可以将这些结果直接绑定到用户界面。创建fetchRequest需要两条信息:
- 要查询的实体
- 以及确定返回结果顺序的排序描述符。
Swift
struct FFCoreDataFetchRequest: View {
@Environment(\.managedObjectContext) var managedObjectContext
// SortDescriptors参数是一个数组,所以可以提供尽可能多的排序选项
@FetchRequest(sortDescriptors: []) var languages: FetchedResults<ProgrammingLanguage>
var body: some View {
List(languages) { language in
Text(language.name ?? "UnKnown")
}
}
}
调试结果
看似简单的代码就成功获得了数据,其实是上面许多步骤后的结果
5、如何使用predicate过滤CoreData的fetchRequests
CoreData的fetchRequest中可以使用Predicates,就像在UIKit中,所有这些都是通向你的@FetchRequest
属性包装器提供一个predicate来实现的。
Swift
struct FFCoreDataFetchRequestsFilter: View {
@Environment(\.managedObjectContext) var managedObjectContext
//创建predicate
@FetchRequest(
sortDescriptors: [],
predicate: NSPredicate(format: "name == %@", "Python")
) var languages: FetchedResults<ProgrammingLanguage>
//目前@FetchRequest使用的是标准的CoreData的Predicate,也可以创建复合Predicate
var body: some View {
List(languages) { language in
Text(language.name ?? "UnKnown")
}
}
}
6、如何从SwiftUI视图添加CoreData对象
在SwiftUI中保存core data对象的工作方式与在SwiftUI之外的工作往事完全相同,访问托管对象的context
,在该上下文中创建你的类型的实例,然后在准备好时保存context。
Swift
struct FFCoreDataAddObjects: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(sortDescriptors: []) var languages: FetchedResults<ProgrammingLanguage>
var body: some View {
//创建实例
Button("Insert Example Languate") {
let language = ProgrammingLanguage(context: managedObjectContext)
language.name = "Python"
language.creator = "Guido van Rossum"
PersistenceController.shared.save()
}
//在添加一组对象之后,程序会多一组对象。
List(languages) { language in
Text(language.name ?? "UnKnown")
}
}
}
调试结果
7、如何从SwiftUI视图中删除CoreData对象
在SwiftUI中删除CoreData对象与在UIKit中删除他们基本相同,尽管需要跳过一些特殊的步骤才能与SwiftUI视图集成。
Swift
struct FFCoreDataDeleteObjects: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(
sortDescriptors: []
) var languages: FetchedResults<ProgrammingLanguage>
var body: some View {
//无论在哪里显示数据,都向SwiftUI视图添加onDelete修饰符。
List {
ForEach(languages) { language in
Text("Creator: \(language.creator ?? "Anonymous")")
}
.onDelete(perform: { indexSet in
removelanguages(at: indexSet)
})
}
.toolbar(content: {
EditButton()
})
}
func removelanguages(at offsets: IndexSet) {
for index in offsets {
let language = languages[index]
managedObjectContext.delete(language)
}
PersistenceController.shared.save()
}
}
调试结果
8、如何限制获取FetchRequest中的item数量
SwiftUI的@FetchRequest属性包装器非常适合对Object发出简单request,提供排序和过滤功能。但是,如果想调整返回item的数量,那么需要额外设置
Swift
struct FFCoreDataLimit: View {
@Environment(\.managedObjectContext) var managedObjectContext
//创建@FetchRequest
@FetchRequest var languages: FetchedResults<ProgrammingLanguage>
init() {
let request: NSFetchRequest<ProgrammingLanguage> = ProgrammingLanguage.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(keyPath: \ProgrammingLanguage.name, ascending: true)
]
request.fetchLimit = 10
_languages = FetchRequest(fetchRequest: request)
}
var body: some View {
List(languages) { language in
Text(language.name ?? "UnKnown")
}
}
}
9、如何在SwiftUI中使用NSUserActivity
SwiftUI有一个专用的onContinueUserActivity()
修饰符,可以捕获各种NSUserActivity类型,比如来自网络的点击、来自第三方或者Siri启动等等。在AppDelegate中使用application(_:continue:restorationHandler:)
处理过这个问题,但SwiftUI方法更简单的处理这个问题。要实现这个需求,首先要创建一个函数并接受参数NSUserActivity,但是,不需要在App结构体中执行此操作。
Swift
func handleWechatLogin(_ userActivity: NSUserActivity) {
if let id = userActivity.userInfo?["targetContentIdentifier"] {
print("Found identifier \(id)")
}
}
使用方法
Swift
var body: some Scene {
WindowGroup {
FFCoreDataDeleteObjects()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.onContinueUserActivity("targetContentIdentifier", perform: { userActivity in
handleWechatLogin(userActivity)
})
}
.onChange(of: scenePhase) {
persistenceController.save()
}
}