在 SwiftUI 中使用 Core Data 与 CloudKit

发布
更新
字数 1254
阅读 7 分钟
阅读量 1465

在构建 iOS 应用时经常需要持久化存储数据,Core Data 作为 Apple 提供的官方方案之一,现在已经非常成熟了。可以自定义模型,管理对象关系。借助 CloudKit 可以轻松地把数据同步到 iCloud 上。在 SwiftUI 中使用 Core Data 最简单的方式就是创建项目时直接选择 Use Core Data,参考示例代码,可以很轻松的为已有 SwiftUI 项目接入 Core Data。

新项目

在创建项目时使用 SwiftUI 构建 Interface,然后选中 Use Core Data 及其下的 Host in CloudKit。Xcode 会初始化各项配置:

  1. 一个基于 Product Name 命名的 Core Data 数据模型:ProjectName.xcdatamodeld 。同时包含一个实体范例 Item 和默认的配置文件,打开 CONFIGURATIONS 下的 Default 看一下,Used with CloudKit 是被选中的。
  2. Persistence 文件中封装了 PersistenceController,包含了使用 Core Data 一些必须的代码:
    1. 初始化并创建 NSPersistenceCloudKitContainer 实例,如果创建项目时没选中 Host in CloudKit,则使用 NSPersistenceContainer
    2. PersistenceController 单例 shared 和用于 SwiftUI 预览的 preview 实例
  3. 通过 @Enviroment 使用 .managedObjectContextviewContext 注入视图,并使用 @FetchedRequest 创建请求,获取数据

此外要使用 CloudKit 还需要一些额外的配置,可以参考文档:https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/setting_up_core_data_with_cloudkit#3191040,开启 iCloud,启用 CloudKit、Push Notifications 和 Remote Notifications

在已有 SwiftUI 项目添加 Core Data

参照上文各个步骤,手动操作即可。

创建 Core Data 数据模型

Xcode 菜单 File > New > File... (Cmd+N) 创建一个 Core Data 的 Data Model 文件即可,并命名如 DATABASE_NAME,注意这里的文件名会被用作 NSPersistenceCloudKitContainer 的名称。之后可以根据需求创建自己的实体(Entity)了。注意需要 model 的默认配置选中了 Used with CloudKit。

NSManagedObject 子类

默认情况下,Xcode 会自动创建 Entity 类,例如命名为 Item 的 Entity,其对应的类名也是 Item,如果要自定义 Entity 类,首先选中 Entity,然后在 Xcode 右侧的 Inspector 的 Class 一节中,把 Module 设为 Current Product Module,然后 Codegen 使用 Manual/None。之后新建一个 Item.swift 文件

import CoreData

class Item: NSManagedObject {
    @NSManaged var timestamp: Date!
}

// 为了在 SwiftUI 中使用 `ForEach`,需要 `Identifiable` 协议
extension Item: Identifiable {
    var id: NSManagedObjectID { objectID }
}

PersistenceController

大部分代码都是参考 Xcode 自动生成

首先我们需要初始化一个 container 实例

import CoreData

extension String {
    // Model 名称
    static let persistentContainerName = "DATABASE_NAME"
}

struct PersistenceController {
    let container: NSPersistenceCloudKitContainer
    
    init(inMemory: Bool = false) {
        // 1. 初始化
        container = NSPersistentCloudKitContainer(name: .persistentContainerName)

        // 2. 创建一个内存中的临时数据库
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
    	// 3. 加载数据库,并处理错误
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                /*
                在这里处理错误,通常有几个典型错误:
                - 数据库所在目录不存在或因为权限问题等不支持操作
                - 数据库不允许访问,权限不足或手机被锁等
                - 空间不足
                - 数据库迁移出错
                */
                // 如果使用 `fatalError` 函数会生成一个错误日志,并让应用崩溃
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

至此我们使用已创建的 Data Model,初始化并加载了 container,在多数情况下,我们在主线程使用 container.viewContext 对数据库操作,如果操作可能阻塞进程,可以使用后台进程 container.newBackgroundContext()

为了把 context 提供给视图,使用单例模式创建 PersistenceController 实例,需要定义一个静态类常量

static let shared = PersistenceController()

@Enviroment 注入 context

在 app 的启动文件使用 .managedObjectContext 注入到环境变量中

@main
struct YOUR_APP: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

ContentView 中使用 @Enviroment 赋值 context

@Enviroment(\.managedObjectContext) private var context

获取数据

ContentView

// `Item` 是 Entity 名称
@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
    animation: .default)
private var items: FetchedResults<Item>

之后可以使用 ForEach 语句使用 items

保存数据

为了方便保存数据,在 PersistenceConntroller 加入一个保存的方法

func save() {
    let context = container.context

    if context.hasChanges {
        do {
            try context.save()
        } catch {
            // 处理错误
        }
    }
}

根据以往经验,在 App 挂起时我们需要自动保存一下数据,可以通过监听 .scenePhase 实现

@main
struct YOUR_APP: App {
    @Environment(\.scenePhase) private var scenePhase
    
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
        .onChange(of: scenePhase) { newValue in
            persistenceController.save()
        }
    }
}

App Groups 共享数据

有时我们需要在扩展中直接访问 Core Data 数据,首先需要把 Data Model 加入对应的 target,然后修改一下 container 的描述,设置数据库存储的路径

extension String {
    static let appGroupIdentifier = "group.<group name>"
    // Model 名称
    static let persistentContainerName = "DATABASE_NAME"
}

struct PersistenceController {
    //...
    init(inMemory: Bool = false) {
        //...
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        } else {
            container.persistentStoreDescriptions.first!.url = URL.appGroupDirectory?.appendingPathComponent("\(String.persistentContainerName).sqlite")
        }
        //...
    }
}