Core Data模板代码PersistenceController中的坑

文中谈到的问题与Core Data有关,故并不局限于SwiftUI应用或不启用CloudKit的场景。

在开发SwiftUI应用时,结合Core Data with CloudKit来提供跨平台数据同步的是个非常不错的选择。当通过Xcode创建使用Core Data with CloudKit新项目时,会提供一些模板代码,大致如下:

swift 复制代码
struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentCloudKitContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "CoreDataDemo")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

示例代码不多,但提供了一些方便的功能,可以支持应用的初期开发。

PersistenceController中的问题

上述示例中代码存在一些潜在的问题及职责不清晰,随着项目的不断迭代,会逐渐变得更加臃肿及无法扩展。下面我们来分析一下存在的问题。

Swift的单例问题

PersistenceController提供了一个shared的单例属性定义,但PersistenceController本身是一个结构体,即一个值类型。而值类型在赋值给一个变量、常量或者传递给函数的时候会产生一个拷贝,这就破坏了单例模式的设计初衷。

所以在Swift中创建单例时,正常的用法应该是使用引用类型进行声明。

swift 复制代码
class PersistenceController {
    ...
}

或者使用actor进行声明,关于Actor的介绍可以参考:Swift 新并发框架之 actor。

swift 复制代码
actor PersistenceController {
    ...
}

职责过多的初始化方法

init(inMemory: Bool = false)中的代码耦合严重,处理了许多事情。在实际使用中,还会有更多的代码加入进来,整个代码变得更加复杂。问题如下:

  1. 以硬编码的方式创建NSPersistentCloudKitContainer实例。
  2. 通过inMemory参数修改NSPersistentCloudKitContainer的存储行为。
  3. 加载完毕后对NSPersistentCloudKitContainer实例进行配置。

虽然上述问题对这个示例来说无所谓,但在实际应用中会是个非常糟糕的例子。在我的开发过程中,还会碰到了以下需求:

  1. 修改数据库的存储路径到App Group。
  2. 定制不同行为的NSPersistentCloudKitContainer的实例。
  3. 将模型相关的代码转换成SPM,用于在主应用和extension中复用。
  4. 更方便的进行单元测试。

fatalError

虽然模板代码中已经加了注释,需要替换成合适的处理方法。但在开发过程中极少会触发到这个错误,导致容易被忽视。建议的做法是通过Debug宏进行隔离处理,避免线上启动就闪退的可能性。

重构PersistenceController

创建不同的NSPersistentCloudKitContainer的子类是个不错的选择,将对实例的配置及相关方法封装到特定的子类中。这也是苹果推荐的一种方式:Subclass the Persistent Container。

为了更灵活的创建不同的NSPersistentCloudKitContainer实例,在初始化方法中通过配置信息来完成NSPersistentCloudKitContainer实例的创建。

优化init方法

首先,创建一个包含初始化配置信息的结构体。这个结构体中包含了NSPersistentCloudKitContainer的类型信息,允许从外部注入需要创建的子类类型。另外,也支持从外部传入NSManagedObjectModel对象,当将Core Data的相关代码模块化后,通常需要从非mainBundle进行加载,故可以自行创建NSManagedObjectModel实例后,传递到NSPersistentCloudKitContainer的初始化过程中。代码如下:

swift 复制代码
final class MyPersistenceController {
    
    struct Configuration {
        
        let containerClass: NSPersistentCloudKitContainer.Type
        
        let name: String
        
        let managedObjectModel: NSManagedObjectModel?
        
        init(containerClass: NSPersistentCloudKitContainer.Type, name: String, managedObjectModel: NSManagedObjectModel? = nil) {
            self.containerClass = containerClass
            self.name = name
            self.managedObjectModel = managedObjectModel
        }
        
    }
    
    let container: NSPersistentCloudKitContainer
    
    init(_ configuration: MyPersistenceController.Configuration) {
        if let managedObjectModel = configuration.managedObjectModel {
            container = configuration.containerClass.init(name: configuration.name, managedObjectModel: managedObjectModel)
        } else {
            container = configuration.containerClass.init(name: configuration.name)
        }
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
#if DEBUG
                fatalError("Unresolved error \(error), \(error.userInfo)")
#else
                
#endif
            }
        })
    }
    
}

创建NSPersistentCloudKitContainer的子类

针对CloudKit持久化和内存中持久化,创建各自不同的子类MyPersistentCloudKitContainer和MyInMemeryContainer。

Extension已经成为iOS开发中不可缺少的一环,为了能够在extension中访问Core Data的数据,需要修改默认保存的数据库文件目录,可在重写defaultDirectoryURL方法修改默认保存的路径。关于怎么通过App Group创建共享目录这里就不展开了。

swift 复制代码
class MyPersistentCloudKitContainer: NSPersistentCloudKitContainer {
    
//    override class func defaultDirectoryURL() -> URL {
//        FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "xxx")!
//    }
    
    override func loadPersistentStores(completionHandler block: @escaping (NSPersistentStoreDescription, Error?) -> Void) {
        super.loadPersistentStores(completionHandler: block)
        
        viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        viewContext.automaticallyMergesChangesFromParent = true
    }
    
}

class MyInMemeryContainer: NSPersistentCloudKitContainer {
    
    override init(name: String, managedObjectModel model: NSManagedObjectModel) {
        super.init(name: name, managedObjectModel: model)
        
        persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        
        addPreviewData()
    }
    
    func addPreviewData() {
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
    }
    
}

创建不同的静态属性

通过扩展的方式,创建不同的静态属性,可以根据实际的情况创建不同的NSPersistentCloudKitContainer配置。比如:可以提供一个专门用于在extension访问配置,禁用掉CloudKit相关的配置属性。

swift 复制代码
extension MyPersistenceController {
    
    static var managedObjectModel: NSManagedObjectModel = {
        NSManagedObjectModel.mergedModel(from: [.main])!
    }()
    
    static var `default`: MyPersistenceController = {
        let configuration: MyPersistenceController.Configuration = .init(
            containerClass: MyPersistentCloudKitContainer.self,
            name: "CoreDataDemo",
            managedObjectModel: managedObjectModel)
        
        return MyPersistenceController(configuration)
    }()
    
    static var preview: MyPersistenceController = {
        MyPersistenceController(.init(containerClass: MyInMemeryContainer.self, name: "CoreDataDemo"))
    }()
    
}

总结

通过在初始化方法中注入创建NSPersistentCloudKitContainer所需的参数,完成了对NSPersistentCloudKitContainer的强依赖。然后,通过创建不同的NSPersistentCloudKitContainer子类封装不同的配置。最终可以非常方便的创建不同的PersistenceController实例,应用到不同的场景中。

完整Demo代码: Github地址

相关推荐
vx_Biye_Design2 小时前
django就业信息推荐系统61953-计算机课程设计、毕业设计
java·vue.js·spring boot·python·架构·django·课程设计
七夜zippoe2 小时前
多 Agent 协作架构:Pipeline 模式——串行流水线设计与实战
ai·架构·pipeline·agent·串行流水线
EatFan2 小时前
从云原生到AI原生:2026后端架构“三驾马车”(事件驱动、虚拟线程、AI Agent内嵌)演进解析
spring boot·云原生·架构·虚拟线程·ai-native·ai agent·spring ai
龙亘川2 小时前
探索智慧文化服务:建设初衷、整体架构、技术支撑、发展现状与实践影响
大数据·数据库·架构
硅基手札3 小时前
【linux内核专栏 09】内核模块与 kbuild
linux·架构
代码山河4 小时前
Java的前世今生:从Oak语言到Java 23的发展史
java·学习·架构·教程·面向对象·项目
新鲜势力呀4 小时前
PHP 文件上传系统实战:从上传卡顿到分片上传 + 秒传 + 云存储架构完整优化方案
开发语言·架构·php
OnlineProxy6 小时前
企业级代理服务器架构:合规性、NDA 诚信与灰色 P2P 网络风险防范
网络·架构·p2p
weixin_3077791314 小时前
有限产能智能排产与动态重排智能体:从需求解构到技术实现
开发语言·人工智能·算法·架构
code_slave(码畜)17 小时前
微服务架构落地:基础服务 —— 报表服务(中篇:元数据、查询引擎、缓存与权限落地)
spring boot·spring cloud·缓存·微服务·架构