作为 Core Data 的继任者,全新的 SwiftData 结构在 WWDC 2023 上正式发布。SwiftData 预计会在未来很长一段时间内成为苹果生态体系的首要目标图管理和数据耐久化解决方案,为开发者供给服务与支撑。本文将评论,在不运用 Core Data 数据栈的情况下,开发者如安在 SwiftData 中调用 Core Data 供给的高档功用,以扩展 SwiftData 现在的能力。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公众号:【肘子的Swift记事本】

SwiftData 当时的困境

与 Core Data 相比,SwiftData 在数据模型声明、类型安全、线程安全、以及与 SwiftUI 整合等多个方面进行了全面提升。其间,它基于 Swift 宏功用的数据模型创立机制、类型安全的谓词体系、依靠 Actor 完结的线程安全以及与 Observation 结构的紧密结合,使得 SwiftData 更契合现代编程的需求。

然而,可能是因为准备时间缺少,当时版别的 SwiftData 还无法完结 Core Data 中的一些高档功用。这就给想尝试 SwiftData 的开发者带来了必定的困扰。即使,开发者能够接受将项目的最小部署环境设置为最新的体系版别( iOS 17、macOS 14 等),也难免需求在项目中同步创立一套基于 Core Data 的数据模型和数据栈,以完结 SwiftData 所缺少的功用。

如此一来,SwiftData 在数据模型声明上的优势便当然无存,不仅增加了工作量,开发者还需求面对怎么处理两个数据结构、模型版别之间的协作问题。仅为完结一些高档功用,就在 SwiftData 的项目中创立一套并行的 Core Data 代码,无疑是非常不经济的。

正是因为上述困难,我一向难以下定决心在新项目中运用 SwiftData。

解决 SwiftData 困境的思路

尽管 SwiftData 在表现上与 Core Data 存在很大差异,可是它的中心根底仍然是 Core Data,苹果运用了 Swift 言语的新功用,用契合当代编程风格的设计思想,对 Core Data 进行了二次构建。这不仅使 SwiftData 承继了 Core Data 在数据耐久化范畴的稳定特质,也意味着 SwiftData 的部分要害组件背面对应着特定的 Core Data 目标。假如咱们能够提取出这些目标,在安全的环境中进行有限度的运用,就能够在 SwiftData 中运用 Core Data 的高档功用。

经过 Swift 言语供给的反射 ( Mirror ) 功用,咱们能够从 SwiftData 的某些组件中提取出需求的 Core Data 目标,例如从 PersistentModel 中提取出 NSManagedObject,从 ModelContext 中提取出 NSManagedContext。别的,SwiftData 的 PersistentIdentifier 契合 Codable 协议,这使咱们能够在它与 NSManagedObjectID 之间进行转化。

SwiftDataKit

依据前文的思路,我开发了 SwiftDataKit 库,它允许开发者运用 SwiftData 组件背面的 Core Data 目标,以完结当时版别无法完结的功用。

例如,下面是从 ModelContext 中提取 NSManagedObjectContext 的代码示例:

public extension ModelContext {
    // Computed property to access the underlying NSManagedObjectContext
    var managedObjectContext: NSManagedObjectContext? {
        guard let managedObjectContext = getMirrorChildValue(of: self, childName: "_nsContext") as? NSManagedObjectContext else {
            return nil
        }
        return managedObjectContext
    }
    // Computed property to access the NSPersistentStoreCoordinator
    var coordinator: NSPersistentStoreCoordinator? {
        managedObjectContext?.persistentStoreCoordinator
    }
}
func getMirrorChildValue(of object: Any, childName: String) -> Any? {
    guard let child = Mirror(reflecting: object).children.first(where: { $0.label == childName }) else {
        return nil
    }
    return child.value
}

接下来,我将经过几个具体案例,简要介绍 SwiftDataKit 的运用办法和注意事项。

SwiftDataKit 是一个实验性质的库。因为 SwiftData API 仍在快速演化中,我主张只有了解其完结原理且明确风险的有经验开发者,在特定场景下慎重运用。

利用 NSManagedObjectContext 完结分组计数

在某些场景下,咱们需求对数据进行分组后计数,比如计算不同出生年份的学生人数。

@Model
class Student {
    var name: String
    var birthOfYear: Int
    init(name: String, birthOfYear: Int) {
        self.name = name
        self.birthOfYear = birthOfYear
    }
}

SwiftData 的新谓词体系现在尚不支撑分组计算,运用原生办法如下所示:

func birthYearCountByQuery() -> [Int: Int] {
    let description = FetchDescriptor<Student>(sortBy: [.init(\Student.birthOfYear, order: .forward)])
    let students = (try? modelContext.fetch(description)) ?? []
    let result: [Int: Int] = students.reduce(into: [:]) { result, student in
        let count = result[student.birthOfYear, default: 0]
        result[student.birthOfYear] = count + 1
    }
    return result
}

开发者需获取悉数数据在内存中进行分组计算。数据量大时,这种办法对性能和内存占用的影响极大。

有了 SwiftDataKit,咱们能够直接运用 ModelContext 底层的 NSManagedObjectContext,经过创立 NSExpressionDescription,在 SQLite 数据库端完结该操作。

func birthYearCountByKit() -> [Int: Int] {
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Student")
    fetchRequest.propertiesToGroupBy = ["birthOfYear"]
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "birthOfYear", ascending: true)]
    fetchRequest.resultType = .dictionaryResultType
    let expressDescription = NSExpressionDescription()
    expressDescription.resultType = .integer64
    expressDescription.name = "count"
    let year = NSExpression(forKeyPath: "birthOfYear")
    let express = NSExpression(forFunction: "count:", arguments: [year])
    expressDescription.expression = express
    fetchRequest.propertiesToFetch = ["birthOfYear", expressDescription]
    // modelContext.managedObjectContext, use NSManagedObjectContext directly
    let fetchResult = (try? modelContext.managedObjectContext?.fetch(fetchRequest) as? [[String: Any]]) ?? []
    let result: [Int: Int] = fetchResult.reduce(into: [:]) { result, element in
        result[element["birthOfYear"] as! Int] = (element["count"] as! Int?) ?? 0
    }
    return result
}

在 10000 条数据的测验中,基于 SwiftDataKit 的完结办法,功率是原生办法的 4 至 5 倍,内存占用也少了许多。

运用 SwiftDataKit 时有几点需求注意:

  • 尽管未声明 Core Data 版别的数据模型类型,但能够用字符串办法拜访 Entity 和特点。默许情况下,SwiftData 中的模型类型名对应 Entity 名,变量名对应特点名。
  • 不引荐运用 setPrimitiveValue(value:, forKey:)value(forKey:) 等办法读写 NSManagedObject 特点数据,缺少编译查看。
  • SwiftData 运用 Actor 保证数据操作在 ModelContext 地点线程中进行,所以在 Actor 办法内不需采用 context.perform 防止线程问题。
@ModelActor
actor StudentHandler {
    func birthYearCountByKit() -> [Int: Int] {
        ...
        // No need to use modelContext.managedObjectContext.perform { ... }
    }
    func birthYearCountByQuery() -> [Int: Int] {
        ...
    }
}
  • 与 Core Data 能够明确创立私有上下文( 运行于非主线程)不同,经过 @ModelActor 创立的 actor 实例所绑定的线程与创立时的上下文有关( _inheritActorContext )。

将 PersistentModel 转化为 NSManagedObject,完结子查询

在 Core Data 中,开发者能够经过创立子查询(SubQuery)谓词,直接在 SQLite 端完结嵌套查询,这对某些场景是必不可缺的功用。

比如咱们有以下数据模型定义:

@Model
class ArticleCollection {
    var name: String
    @Relationship(deleteRule: .nullify)
    var articles: [Article]
    init(name: String, articles: [Article] = []) {
        self.name = name
        self.articles = articles
    }
}
@Model
class Article {
    var name: String
    @Relationship(deleteRule: .nullify)
    var category: Category?
    @Relationship(deleteRule: .nullify)
    var collection: ArticleCollection?
    init(name: String, category: Category? = nil, collection: ArticleCollection? = nil) {
        self.name = name
        self.category = category
        self.collection = collection
    }
}
@Model
class Category {
    var name: String
    @Relationship(deleteRule: .nullify)
    var articles: [Article]
    init(name: String, articles: [Article] = []) {
        self.name = name
        self.articles = articles
    }
    enum Name: String, CaseIterable {
        case tech, health, travel
    }
}

在这种模型关系( ArticleCollection <-->> Article <<--> Category )下,咱们想查询有多少个 ArticleCollection 中的任意 Article 归于特定的 Category。

当时,运用 SwiftData 的原生办法如下所示:

func getCollectCountByCategoryByQuery(categoryName: String) -> Int {
    guard let category = getCategory(by: categoryName) else {
        fatalError("Can't get tag by name:\(categoryName)")
    }
    let description = FetchDescriptor<ArticleCollection>()
    let collections = (try? modelContext.fetch(description)) ?? []
    let count = collections.filter { collection in
        !(collection.articles).filter { article in
            article.category == category
        }.isEmpty
    }.count
    return count
}

与上文的办法相似,需求获取悉数数据在内存中进行过滤计算。

经过将 PersistentModel 转化成 NSManagedObject,咱们能够用包含子查询的谓词提高功率:

func getCollectCountByCategoryByKit(categoryName: String) -> Int {
    guard let category = getCategory(by: categoryName) else {
        fatalError("Can't get tag by name:\(categoryName)")
    }
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ArticleCollection")
    // get NSManagedObject by category.managedObject
    guard let categoryObject = category.managedObject else {
        fatalError("can't get managedObject from \(category)")
    }
    // use NSManagedObject in Predicate
    let predicate = NSPredicate(format: "SUBQUERY(articles,$article,$article.category == %@).@count > 0", categoryObject)
    fetchRequest.predicate = predicate
    return (try? modelContext.managedObjectContext?.count(for: fetchRequest)) ?? 0
}
// fetch category by name
func getCategory(by name: String) -> Category? {
    let predicate = #Predicate<Category> {
        $0.name == name
    }
    let categoryDescription = FetchDescriptor<Category>(predicate: predicate)
    return try? modelContext.fetch(categoryDescription).first
}

在示例中,是经过 Category 的 name 来创立谓词并获取数据。通常咱们也会用 PersistentIdentifier 在不同 ModelContext 间进行安全传递。这时能够:

func getCategory(by categoryID:PersistentIdentifier) -> Category? {
    let predicate = #Predicate<Category> {
        $0.id == categoryID
    }
    let categoryDescription = FetchDescriptor<Category>(predicate: predicate)
    return try? modelContext.fetch(categoryDescription).first
}

SwiftData 在多线程开发方面与 Core Data 相似,仅仅形式不同。阅读 关于 Core Data 并发编程的几点提示 一文,了解 Core Data 在这方面的更多注意事项。

将 NSManagedObject 转化为 PersistentModel

有人可能会问,咱们只能用 SwiftDataKit 回来计算数据吗?是否能够将 NSFetchRequest 获取的 NSManagedObject 转化为 PersistentModel 在 SwiftData 中运用?

与前面需求相似,这儿咱们想获取有哪些 ArticleCollection 的任意 Article 归于特定 Category。

利用 PersistentIdentifier 的 decode 构造办法,SwiftDataKit 支撑将 NSManagedObjectID 转化为 PersistentIdentifier,用下面的代码,咱们将取得一切契合条件的 ActicleCategory 的 PersistentIdentifier。

func getCollectPersistentIdentifiersByTagByKit(categoryName: String) -> [PersistentIdentifier] {
    guard let category = getCategory(by: categoryName) else {
        fatalError("Can't get tag by name:\(categoryName)")
    }
    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "ArticleCollection")
    guard let categoryObject = category.managedObject else {
        fatalError("can't get managedObject from \(category)")
    }
    let predicate = NSPredicate(format: "SUBQUERY(articles,$article,$article.category == %@).@count > 0", categoryObject)
    fetchRequest.predicate = predicate
    fetchRequest.sortDescriptors = [.init(key: "name", ascending: true)]
    let collections = (try? modelContext.managedObjectContext?.fetch(fetchRequest)) ?? []
    // convert NSManageObjectID to PersistentIdentifier by SwiftDataKit
    return collections.compactMap(\.objectID.persistentIdentifier)
}

然后依据 PersistentIdentifier 获取对应的 PersistentModel 实例:

func convertIdentifierToModel<T: PersistentModel>(ids: [PersistentIdentifier], type: T.Type) -> [T] {
    ids.compactMap { self[$0, as: type] }
}

在 SwiftData 中,供给了两种不运用谓词,经过 PersistentIdentifier 获取 PersistentModel 的办法,用法和差异我在这篇 推文 中进行了说明。

SwiftDataKit:让你在 SwiftData 中使用 Core Data 的高级功能

经过这些示例,开发者基本能够在不创立 Core Data 数据模型和数据栈的情况下,在 SwiftData 中运用 Core Data 各种高档功用。

与 Core Data Stack 进行数据交换

假如直接操作 SwiftData 底层目标仍无法满足需求,则需求创立并行的 Core Data 数据模型和数据栈,并在 SwiftData 和 Core Data 代码间进行数据交换。

因为 NSManagedObjectID 在不同 NSPersistentStoreCoordinator 间无法保持一致,能够运用 SwiftDataKit 供给的如下功用:

  • 将 PersistentIdentifier 转化为 uriRepresentation
  • 将 uriRepresentation 转为 PersistentIdentifier
// convert persistentIdentifier to uriRepresentation
category.id.uriRepresentation
// convert uriRepresentation to persistentIdentifier
uriRepresentation.persistentIdentifier

这样就能够在 SwiftData 栈与 Core Data 栈之间安全地传递数据。

总结

经过本文的评论和示例,咱们能够看到,尽管当时 SwiftData 还无法完结 Core Data 的一切高档功用,但经过 SwiftDataKit 供给的接口与东西,开发者能够相对轻松地在 SwiftData 中持续运用 Core Data 的优异特性。这将大大降低新项目全面采用 SwiftData 的门槛,无需同步维护一套 Core Data 的数据模型与数据栈。

当然,SwiftDataKit 仅是一个过渡时期的解决方案。跟着 SwiftData 不断地完善,它会加入越来越多的新功用。咱们期待在不久的将来,SwiftData 能成为一个功用完备、简略易用的下一代 Core Data。

PS:SwiftDataKit 现在供给的功用还很有限,欢迎更多的开发者能够参与该项目,让大家能够尽早享受到运用 SwiftData 开发所能带来的爽快感。

欢迎你经过 Twitter、 Discord 频道 或博客的留言板与我进行交流。

订阅下方的 邮件列表,能够及时取得每周最新文章。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公众号:【肘子的Swift记事本】