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

在开发SwiftUI运用时,结合Core Data with CloudKit来供给跨平台数据同步的是个十分不错的选择。当经过Xcode创立运用Core Data with CloudKit新项目时,会供给一些模板代码,大致如下:

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中创立单例时,正常的用法应该是运用引用类型进行声明。

class PersistenceController {
    ...
}

或许运用actor进行声明,关于Actor的介绍能够参阅:Swift 新并发结构之 actor

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的初始化过程中。代码如下:

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创立共享目录这儿就不展开了。

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相关的配置特点。

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地址