├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ └── xcschemes │ └── CoreDataKit.xcscheme ├── README.md ├── Sources └── CoreDataKit │ ├── Documentation.docc │ └── CoreDataKit.md │ ├── NSFetchRequest+Extension.swift │ ├── NSManagedObject+Extension.swift │ ├── ManagedObject.swift │ ├── NSManagedObjectContext+Extension.swift │ ├── NSPersistentContainer+Extension.swift │ ├── CoreDataContainer.swift │ └── CoreDataCloudKitContainer.swift ├── Package.swift └── Tests └── CoreDataKitTests ├── TestData └── CoreDataKit.xcdatamodeld │ └── Model.xcdatamodel │ └── contents ├── FetchRequestTests.swift ├── CoreDataContainerTests.swift └── CoreDataCloudKitContainerTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoreDataKit 2 | 3 | Core Data helper library. 4 | 5 | ## Overview 6 | 7 | This package contains a subclass of `NSPersistentContainer` (and `NSPersistentCloudKitContainer`), and a number of useful Core Data extensions. 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/CoreDataKit/Documentation.docc/CoreDataKit.md: -------------------------------------------------------------------------------- 1 | # ``CoreDataKit`` 2 | 3 | A Core Data Helper package. 4 | 5 | ## Overview 6 | 7 | This package contains a subclasses of `NSPersistentContainer` and `NSPersistentCloudKitContainer` and a number of useful Core Data extensions. 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` 14 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "CoreDataKit", 7 | defaultLocalization: "en", 8 | platforms: [ 9 | .iOS(.v14), .macOS(.v11), .macCatalyst(.v14), .tvOS(.v14), .watchOS(.v7) 10 | ], 11 | products: [ 12 | .library( 13 | name: "CoreDataKit", 14 | targets: ["CoreDataKit"]), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "CoreDataKit", 19 | dependencies: []), 20 | .testTarget( 21 | name: "CoreDataKitTests", 22 | dependencies: ["CoreDataKit"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Tests/CoreDataKitTests/TestData/CoreDataKit.xcdatamodeld/Model.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Sources/CoreDataKit/NSFetchRequest+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | 31 | public extension NSFetchRequest { 32 | /// Combine predicates using a logical AND 33 | /// - Parameter predicate: predicate to add 34 | @objc func andPredicate(predicate: NSPredicate) { 35 | guard let currentPredicate = self.predicate else { 36 | self.predicate = predicate 37 | return 38 | } 39 | self.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [currentPredicate, predicate]) 40 | } 41 | 42 | /// Combine predicates using a logical OR 43 | /// - Parameter predicate: predicate to add 44 | @objc func orPredicate(predicate: NSPredicate) { 45 | guard let currentPredicate = self.predicate else { 46 | self.predicate = predicate 47 | return 48 | } 49 | self.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: [currentPredicate, predicate]) 50 | } 51 | 52 | /// Logical NOT 53 | @objc func notPredicate() { 54 | guard let currentPredicate = self.predicate else { 55 | return 56 | } 57 | self.predicate = NSCompoundPredicate(notPredicateWithSubpredicate: currentPredicate) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/CoreDataKitTests/FetchRequestTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021-2025 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | import CoreDataKit 31 | import Testing 32 | 33 | struct FetchRequestTests { 34 | @Test func andPredicate() throws { 35 | let request = NSFetchRequest() 36 | request.predicate = NSPredicate(format: "flag == true") 37 | request.andPredicate(predicate: NSPredicate(format: "green == false")) 38 | let result = try #require(request.predicate?.predicateFormat) 39 | #expect(result == "flag == 1 AND green == 0") 40 | } 41 | 42 | @Test func orPredicate() throws { 43 | let request = NSFetchRequest() 44 | request.predicate = NSPredicate(format: "flag == true") 45 | request.orPredicate(predicate: NSPredicate(format: "green == false")) 46 | let result = try #require(request.predicate?.predicateFormat) 47 | #expect(result == "flag == 1 OR green == 0") 48 | } 49 | 50 | @Test func notPredicate() throws { 51 | let request = NSFetchRequest() 52 | request.predicate = NSPredicate(format: "flag == true") 53 | request.notPredicate() 54 | let result = try #require(request.predicate?.predicateFormat) 55 | #expect(result == "NOT flag == 1") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/CoreDataKit/NSManagedObject+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | 31 | extension NSManagedObject { 32 | /// Create a new managed object, inserting it into the 33 | /// specified managed object context. 34 | /// 35 | /// This method calls the designated initializer 36 | /// `init(entity:insertInto:)` using the class name to create 37 | /// the entity description. This avoids a problem, often seen 38 | /// in unit tests, where the convenience initializer 39 | /// `init(context:)` generates a warning that multiple 40 | /// `NSEntityDescriptions` claim the `NSManagedObject` subclass 41 | /// when the managed object model is loaded more than once. 42 | /// 43 | /// - Note: This method assumes the class name matches the 44 | /// entity name in the model. It will crash if the entity 45 | /// cannot be found in the managed object model. 46 | /// 47 | /// - Parameter context: The context to insert the new object 48 | /// into. 49 | public convenience init(insertInto context: NSManagedObjectContext) { 50 | let entityName = String(describing: type(of: self)) 51 | guard let entity = NSEntityDescription.entity(forEntityName: entityName, in: context) else { 52 | fatalError("\(entityName) not found") 53 | } 54 | self.init(entity: entity, insertInto: context) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/CoreDataKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 59 | 60 | 63 | 64 | 65 | 66 | 72 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /Sources/CoreDataKit/ManagedObject.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | 31 | public protocol ManagedObject: NSFetchRequestResult { 32 | static var entityName: String { get } 33 | static var defaultSortDescriptors: [NSSortDescriptor] { get } 34 | static var fetchRequest: NSFetchRequest { get } 35 | static var sortedFetchRequest: NSFetchRequest { get } 36 | } 37 | 38 | extension ManagedObject { 39 | public static var defaultSortDescriptors: [NSSortDescriptor] { [] } 40 | 41 | public static var fetchRequest: NSFetchRequest { 42 | NSFetchRequest(entityName: entityName) 43 | } 44 | 45 | public static var sortedFetchRequest: NSFetchRequest { 46 | let request = fetchRequest 47 | request.sortDescriptors = defaultSortDescriptors 48 | return request 49 | } 50 | } 51 | 52 | extension ManagedObject where Self: NSManagedObject { 53 | public typealias configureRequest = (NSFetchRequest) -> Void 54 | 55 | public static func countRequest(_ context: NSManagedObjectContext, configure: configureRequest? = nil) -> Int { 56 | let request = fetchRequest 57 | if let configure = configure { configure(request) } 58 | let count = try? context.count(for: request) 59 | return count ?? 0 60 | } 61 | 62 | public static func fetch(_ context: NSManagedObjectContext, configure: configureRequest? = nil) -> Result<[Self], Error> { 63 | let request = fetchRequest 64 | if let configure = configure { configure(request) } 65 | return Result { try context.fetch(request) } 66 | } 67 | 68 | public static func fetchFirst(_ context: NSManagedObjectContext, configure: configureRequest? = nil) -> Self? { 69 | let result = Self.fetch(context) { request in 70 | if let configure = configure { configure(request) } 71 | request.returnsObjectsAsFaults = false 72 | request.fetchLimit = 1 73 | } 74 | 75 | guard case let .success(objects) = result, 76 | let object = objects.first 77 | else { 78 | return nil 79 | } 80 | return object 81 | } 82 | 83 | public static func fetchOrCreate(_ context: NSManagedObjectContext, matching predicate: NSPredicate) -> Self { 84 | let result = fetch(context) { request in 85 | request.predicate = predicate 86 | request.returnsObjectsAsFaults = false 87 | request.fetchLimit = 1 88 | } 89 | 90 | guard case let .success(objects) = result, 91 | let object = objects.first 92 | else { 93 | return Self(context: context) 94 | } 95 | 96 | return object 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/CoreDataKit/NSManagedObjectContext+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | 31 | extension NSManagedObjectContext { 32 | /// Save the context 33 | /// 34 | /// If the current context has unsaved changes attempts 35 | /// to commit to parent store. 36 | /// 37 | /// - Returns: nil if successful or an NSError. 38 | /// 39 | /// - Note: You must perform this operation on the queue 40 | /// specified for the context. 41 | public func saveIfChanged() -> NSError? { 42 | guard hasChanges else { return nil } 43 | do { 44 | try save() 45 | return nil 46 | } catch { 47 | return error as NSError 48 | } 49 | } 50 | 51 | /// Save the context 52 | /// 53 | /// If the current context has unsaved changes attempts 54 | /// to commit to parent store. If the save operation fails, 55 | /// rollback to the last committed state. 56 | /// 57 | /// - Returns: nil if successful or an NSError. 58 | /// 59 | /// - Note: You must perform this operation on the queue 60 | /// specified for the context. 61 | public func saveOrRollback() -> NSError? { 62 | guard hasChanges else { return nil } 63 | do { 64 | try save() 65 | return nil 66 | } catch { 67 | rollback() 68 | return error as NSError 69 | } 70 | } 71 | 72 | /// Asynchronously commit any unsaved changes on 73 | /// the context's queue. If the save fails the 74 | /// context is rolled back to the last 75 | /// committed state. 76 | public func performSaveOrRollback() { 77 | perform { 78 | _ = self.saveOrRollback() 79 | } 80 | } 81 | 82 | /// Asynchronously perform a block then commit any 83 | /// unsaved changes on the context's queue. If the 84 | /// save fails the context is rolled back to the 85 | /// last committed state. 86 | public func performThenSave(block: @escaping() -> ()) { 87 | perform { 88 | block() 89 | _ = self.saveOrRollback() 90 | } 91 | } 92 | 93 | /// Batch delete objects and optionally merge changes into multiple 94 | /// contexts. 95 | /// 96 | /// A batch delete is faster to delete multiple objects but it bypasses 97 | /// any validation rules and does not automatically update any of the 98 | /// objects which may be in memory unless you merge the change into the 99 | /// context. 100 | /// 101 | /// The batch delete is performed on a background managed object context. 102 | /// 103 | /// - Parameters: 104 | /// - objectIDs: NSManagedObjectIDs of objects to delete. 105 | /// - contexts: Optional array of managed object contexts which will 106 | /// have the changes merged. 107 | public func batchDelete(objectIDs: [NSManagedObjectID], mergeInto contexts: [NSManagedObjectContext]? = nil) throws { 108 | guard !objectIDs.isEmpty else { return } 109 | 110 | let request = NSBatchDeleteRequest(objectIDs: objectIDs) 111 | request.resultType = .resultTypeObjectIDs 112 | let deleteResult = try execute(request) as? NSBatchDeleteResult 113 | 114 | if let contexts = contexts, 115 | let deletedIDs = deleteResult?.result as? [NSManagedObjectID] { 116 | let changes = [NSDeletedObjectsKey: deletedIDs] 117 | NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: contexts) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/CoreDataKit/NSPersistentContainer+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | 31 | extension NSPersistentContainer { 32 | /// Export the persistent store to a new location 33 | /// 34 | /// The first persistent store, if any, is migrated 35 | /// to the new location. 36 | /// 37 | /// The SQLite WAL mode is disabled on the exported 38 | /// store to force a checkpoint of all changes. 39 | /// 40 | /// - Parameter url: Destination URL 41 | @available(macOS 12.0, iOS 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) 42 | public func exportStore(to url: URL) throws { 43 | guard let store = persistentStoreCoordinator.persistentStores.first else { 44 | return 45 | } 46 | 47 | // Options to disable WAL mode 48 | let options = [ 49 | NSSQLitePragmasOption: ["journal_mode": "DELETE"] 50 | ] 51 | 52 | // If the destination store exists, empty the database. 53 | // This doesn't delete the database file. 54 | _ = try persistentStoreCoordinator.destroyPersistentStore( 55 | at: url, 56 | type: .sqlite, 57 | options: options 58 | ) 59 | 60 | // Move the existing store to the new location 61 | _ = try persistentStoreCoordinator.migratePersistentStore( 62 | store, 63 | to: url, 64 | options: options, 65 | type: .sqlite 66 | ) 67 | } 68 | 69 | /// Remove all stores from the persistent store coordinator. 70 | public func removeStores() throws { 71 | for store in persistentStoreCoordinator.persistentStores { 72 | try persistentStoreCoordinator.remove(store) 73 | } 74 | } 75 | 76 | /// Delete the SQLite files for a store. 77 | /// 78 | /// Deletes the `.db`, `.db-shm` and `.db-wal` files 79 | /// for an SQLite persistent store. 80 | /// 81 | /// - Parameters: 82 | /// - name: The name of the store (without extension). 83 | /// - pathExtension: File extension. Default is "db". 84 | /// - directoryURL: Optional URL for the containing directory. 85 | /// Defaults to nil which uses the default container directory. 86 | public class func deleteStore(name: String, pathExtension: String = "db", directoryURL: URL? = nil) { 87 | let baseURL = directoryURL ?? CoreDataContainer.defaultDirectoryURL() 88 | 89 | let fileURL: URL 90 | if #available(macOS 13.0, iOS 16.0, macCatalyst 16.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *) { 91 | fileURL = baseURL.appending(path: name, directoryHint: .notDirectory) 92 | } else { 93 | fileURL = baseURL.appendingPathComponent(name, isDirectory: false) 94 | } 95 | 96 | let dbURL = fileURL.appendingPathExtension(pathExtension) 97 | try? FileManager.default.removeItem(at: dbURL) 98 | 99 | let shmURL = fileURL.appendingPathExtension("\(pathExtension)-shm") 100 | try? FileManager.default.removeItem(at: shmURL) 101 | 102 | let walURL = fileURL.appendingPathExtension("\(pathExtension)-wal") 103 | try? FileManager.default.removeItem(at: walURL) 104 | 105 | let journalURL = fileURL.appendingPathExtension("\(pathExtension)-journal") 106 | try? FileManager.default.removeItem(at: journalURL) 107 | } 108 | 109 | /// Pin the context to the current store generation. 110 | /// 111 | /// The context is advanced when you call save, merge, 112 | /// or reset the context. 113 | /// 114 | /// - Parameter context: Managed object context to ping 115 | /// - Returns: Error or `nil` if successful. 116 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) 117 | public func pin(_ context: NSManagedObjectContext) -> Error? { 118 | let error: Error? = context.performAndWait { 119 | do { 120 | try context.setQueryGenerationFrom(.current) 121 | return nil 122 | } catch { 123 | return error 124 | } 125 | } 126 | return error 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Tests/CoreDataKitTests/CoreDataContainerTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021-2025 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | import CoreDataKit 31 | import Testing 32 | 33 | @MainActor struct CoreDataContainerTests { 34 | private let modelName = "CoreDataKit" 35 | private let container: CoreDataContainer 36 | 37 | init() { 38 | container = CoreDataContainer( 39 | name: modelName, bundle: .module, inMemory: true) 40 | } 41 | 42 | @Test func containerName() throws { 43 | #expect(modelName == container.name) 44 | } 45 | 46 | @Test func loadMOM() throws { 47 | let entities = container.managedObjectModel.entities 48 | #expect(entities.count == 1) 49 | } 50 | 51 | @Test func defaultStoreURL() throws { 52 | #expect(container.storeURL != nil) 53 | } 54 | 55 | @Test func storeNotLoaded() throws { 56 | #expect(!container.isStoreLoaded) 57 | } 58 | 59 | @Test func shouldMigrateStoreAutomatically() throws { 60 | let storeDescription = try #require( 61 | container.persistentStoreDescriptions.first) 62 | #expect(storeDescription.shouldMigrateStoreAutomatically) 63 | } 64 | 65 | @Test func shouldInferMappingModelAutomatically() throws { 66 | let storeDescription = try #require( 67 | container.persistentStoreDescriptions.first) 68 | #expect( 69 | storeDescription.shouldInferMappingModelAutomatically) 70 | } 71 | 72 | @Test func isNotReadOnly() throws { 73 | let storeDescription = try #require( 74 | container.persistentStoreDescriptions.first) 75 | #expect(!storeDescription.isReadOnly) 76 | } 77 | 78 | @Test func shouldAddStoreAsynchronouslyTrueByDefault() throws { 79 | let container = CoreDataContainer(name: modelName, bundle: .module) 80 | let storeDescription = try #require( 81 | container.persistentStoreDescriptions.first) 82 | #expect(storeDescription.shouldAddStoreAsynchronously) 83 | } 84 | 85 | @Test func shouldAddStoreAsynchronouslyFalseInMemory() throws { 86 | let storeDescription = try #require( 87 | container.persistentStoreDescriptions.first) 88 | #expect(!storeDescription.shouldAddStoreAsynchronously) 89 | } 90 | 91 | @Test func shouldAddStoreSynchronously() throws { 92 | let container = CoreDataContainer(name: modelName, bundle: .module, shouldAddStoreAsynchronously: false) 93 | let storeDescription = try #require( 94 | container.persistentStoreDescriptions.first) 95 | #expect(storeDescription.shouldAddStoreAsynchronously == false) 96 | } 97 | 98 | @Test func historyTrackingKeyTrueByDefault() throws { 99 | let storeDescription = try #require( 100 | container.persistentStoreDescriptions.first) 101 | let historyTrackingOption = try #require( 102 | storeDescription.options[NSPersistentHistoryTrackingKey] 103 | as? NSNumber) 104 | #expect(historyTrackingOption.boolValue) 105 | } 106 | 107 | @Test func createStoreInMemory() throws { 108 | let storeDescription = try #require( 109 | container.persistentStoreDescriptions.first) 110 | #expect(storeDescription.url == URL(fileURLWithPath: "/dev/null")) 111 | } 112 | 113 | @Test func createStoreWithCustomURL() throws { 114 | let url = FileManager.default.temporaryDirectory 115 | let container = CoreDataContainer( 116 | name: modelName, bundle: .module, url: url) 117 | let storeDescription = try #require( 118 | container.persistentStoreDescriptions.first) 119 | #expect(storeDescription.url == url) 120 | } 121 | 122 | @Test func inMemoryStoreOverridesCustomURL() throws { 123 | let url = FileManager.default.temporaryDirectory 124 | let container = CoreDataContainer( 125 | name: modelName, bundle: .module, url: url, inMemory: true) 126 | let storeDescription = try #require( 127 | container.persistentStoreDescriptions.first) 128 | #expect(storeDescription.url == URL(fileURLWithPath: "/dev/null")) 129 | } 130 | 131 | @Test func ceateWithMOM() throws { 132 | let momURL = try #require( 133 | Bundle.module.url(forResource: modelName, withExtension: "momd")) 134 | let mom = try #require(NSManagedObjectModel(contentsOf: momURL)) 135 | _ = CoreDataContainer(name: modelName, mom: mom, inMemory: true) 136 | } 137 | 138 | @Test func loadStoreSync() throws { 139 | container.loadPersistentStores { description, error in 140 | #expect(error == nil) 141 | } 142 | #expect(container.isStoreLoaded) 143 | } 144 | 145 | @Test func loadStoreAsync() async throws { 146 | let storeDescription = try #require( 147 | container.persistentStoreDescriptions.first) 148 | storeDescription.shouldAddStoreAsynchronously = true 149 | 150 | await withCheckedContinuation { continuation in 151 | container.loadPersistentStores { description, error in 152 | #expect(error == nil) 153 | continuation.resume() 154 | } 155 | } 156 | 157 | #expect(container.isStoreLoaded) 158 | } 159 | 160 | @Test func viewContextMergesChanges() throws { 161 | container.loadPersistentStores { description, error in 162 | #expect(error == nil) 163 | } 164 | 165 | #expect( 166 | container.viewContext.automaticallyMergesChangesFromParent) 167 | } 168 | 169 | @Test func viewContextName() throws { 170 | container.loadPersistentStores { description, error in 171 | #expect(error == nil) 172 | } 173 | #expect(container.viewContext.name != nil) 174 | } 175 | 176 | @Test func viewContextMergePolicy() throws { 177 | container.loadPersistentStores { description, error in 178 | #expect(error == nil) 179 | } 180 | let policy = try #require( 181 | container.viewContext.mergePolicy as? NSMergePolicy) 182 | #expect(policy == NSMergePolicy.mergeByPropertyObjectTrump) 183 | } 184 | 185 | @Test func defaultTransactionAuthor() throws { 186 | container.loadPersistentStores { description, error in 187 | #expect(error == nil) 188 | } 189 | let author = try #require(container.viewContext.transactionAuthor) 190 | #expect(author == "app") 191 | } 192 | 193 | @Test func customTransactionAuthor() throws { 194 | container.appTransactionAuthorName = "Test" 195 | container.loadPersistentStores { description, error in 196 | #expect(error == nil) 197 | } 198 | let author = try #require(container.viewContext.transactionAuthor) 199 | #expect(author == "Test") 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Tests/CoreDataKitTests/CoreDataCloudKitContainerTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2023-2025 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | import CoreDataKit 31 | import Testing 32 | 33 | @MainActor struct CoreDataCloudKitContainerTests { 34 | private let modelName = "CoreDataKit" 35 | private let container: CoreDataCloudKitContainer 36 | 37 | init() { 38 | container = CoreDataCloudKitContainer(name: modelName, bundle: .module, inMemory: true) 39 | } 40 | 41 | @Test func testContainerName() throws { 42 | #expect(modelName == container.name) 43 | } 44 | 45 | @Test func loadMOM() throws { 46 | let entities = container.managedObjectModel.entities 47 | #expect(entities.count == 1) 48 | } 49 | 50 | @Test func defaultStoreURL() throws { 51 | #expect(container.storeURL != nil) 52 | } 53 | 54 | @Test func shouldMigrateStoreAutomatically() throws { 55 | let storeDescription = try #require( container.persistentStoreDescriptions.first) 56 | #expect(storeDescription.shouldMigrateStoreAutomatically) 57 | } 58 | 59 | @Test func shouldInferMappingModelAutomatically() throws { 60 | let storeDescription = try #require(container.persistentStoreDescriptions.first) 61 | #expect(storeDescription.shouldInferMappingModelAutomatically == true) 62 | } 63 | 64 | @Test func isNotReadOnly() throws { 65 | let storeDescription = try #require(container.persistentStoreDescriptions.first) 66 | #expect(!storeDescription.isReadOnly) 67 | } 68 | 69 | @Test func shouldAddStoreAsynchronouslyTrueByDefault() throws { 70 | let container = CoreDataCloudKitContainer(name: modelName, bundle: .module) 71 | let storeDescription = try #require(container.persistentStoreDescriptions.first) 72 | #expect(storeDescription.shouldAddStoreAsynchronously) 73 | } 74 | 75 | @Test func shouldAddStoreAsynchronouslyFalseInMemory() throws { 76 | let storeDescription = try #require( container.persistentStoreDescriptions.first) 77 | #expect(!storeDescription.shouldAddStoreAsynchronously) 78 | } 79 | 80 | @Test func shouldAddStoreSynchronously() throws { 81 | let container = CoreDataCloudKitContainer(name: modelName, bundle: .module, shouldAddStoreAsynchronously: false) 82 | let storeDescription = try #require( 83 | container.persistentStoreDescriptions.first) 84 | #expect(storeDescription.shouldAddStoreAsynchronously == false) 85 | } 86 | 87 | @Test func historyTrackingKeyTrueByDefault() throws { 88 | let storeDescription = try #require( container.persistentStoreDescriptions.first) 89 | let historyTrackingOption = try #require(storeDescription.options[NSPersistentHistoryTrackingKey] as? NSNumber) 90 | #expect(historyTrackingOption.boolValue) 91 | } 92 | 93 | @Test func remoteStoreNotificationTrueByDefault() throws { 94 | let storeDescription = try #require( container.persistentStoreDescriptions.first) 95 | let remoteChangeOption = try #require(storeDescription.options[NSPersistentStoreRemoteChangeNotificationPostOptionKey] as? NSNumber) 96 | #expect(remoteChangeOption.boolValue) 97 | } 98 | 99 | @Test func syncDisabled() throws { 100 | let container = CoreDataCloudKitContainer(name: modelName, bundle: .module, syncDisabled: true) 101 | let privateStore = try #require(container.persistentStoreDescriptions.first) 102 | let syncStore = try #require(privateStore.copy() as? NSPersistentStoreDescription) 103 | syncStore.url = CoreDataCloudKitContainer.defaultDirectoryURL().appendingPathComponent("sync.db") 104 | syncStore.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "test") 105 | container.persistentStoreDescriptions.append(syncStore) 106 | container.loadPersistentStores { description, error in 107 | #expect(error == nil) 108 | #expect(description.cloudKitContainerOptions == nil) 109 | } 110 | } 111 | 112 | @Test func testCreateStoreInMemory() throws { 113 | let storeDescription = try #require( container.persistentStoreDescriptions.first) 114 | #expect(storeDescription.url == URL(fileURLWithPath: "/dev/null")) 115 | } 116 | 117 | @Test func createStoreWithCustomURL() throws { 118 | let url = FileManager.default.temporaryDirectory 119 | let container = CoreDataCloudKitContainer(name: modelName, bundle: .module, url: url) 120 | let storeDescription = try #require( container.persistentStoreDescriptions.first) 121 | #expect(storeDescription.url == url) 122 | } 123 | 124 | @Test func inMemoryStoreOverridesCustomURL() throws { 125 | let url = FileManager.default.temporaryDirectory 126 | let container = CoreDataCloudKitContainer(name: modelName, bundle: .module, url: url, inMemory: true) 127 | let storeDescription = try #require( container.persistentStoreDescriptions.first) 128 | #expect(storeDescription.url == URL(fileURLWithPath: "/dev/null")) 129 | } 130 | 131 | @Test func ceateWithMOM() throws { 132 | let momURL = try #require(Bundle.module.url(forResource: modelName, withExtension: "momd")) 133 | let mom = try #require(NSManagedObjectModel(contentsOf: momURL)) 134 | _ = CoreDataCloudKitContainer(name: modelName, mom: mom, inMemory: true) 135 | } 136 | 137 | @Test func loadStoreSync() throws { 138 | container.loadPersistentStores { description, error in 139 | #expect(error == nil) 140 | } 141 | } 142 | 143 | @Test func loadStoreAsync() async throws { 144 | let storeDescription = try #require(container.persistentStoreDescriptions.first) 145 | storeDescription.shouldAddStoreAsynchronously = true 146 | 147 | await withCheckedContinuation { continuation in 148 | container.loadPersistentStores { description, error in 149 | #expect(error == nil) 150 | continuation.resume() 151 | } 152 | } 153 | } 154 | 155 | @Test func viewContextMergesChanges() throws { 156 | container.loadPersistentStores { description, error in 157 | #expect(error == nil) 158 | } 159 | 160 | #expect( 161 | container.viewContext.automaticallyMergesChangesFromParent) 162 | } 163 | 164 | @Test func viewContextName() throws { 165 | container.loadPersistentStores { description, error in 166 | #expect(error == nil) 167 | } 168 | #expect(container.viewContext.name != nil) 169 | } 170 | 171 | @Test func viewContextMergePolicy() throws { 172 | container.loadPersistentStores { description, error in 173 | #expect(error == nil) 174 | } 175 | let policy = try #require( 176 | container.viewContext.mergePolicy as? NSMergePolicy) 177 | #expect(policy == NSMergePolicy.mergeByPropertyObjectTrump) 178 | } 179 | 180 | @Test func defaultTransactionAuthor() throws { 181 | container.loadPersistentStores { description, error in 182 | #expect(error == nil) 183 | } 184 | let author = try #require(container.viewContext.transactionAuthor) 185 | #expect(author == "app") 186 | } 187 | 188 | @Test func customTransactionAuthor() throws { 189 | container.appTransactionAuthorName = "Test" 190 | container.loadPersistentStores { description, error in 191 | #expect(error == nil) 192 | } 193 | let author = try #require(container.viewContext.transactionAuthor) 194 | #expect(author == "Test") 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Sources/CoreDataKit/CoreDataContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021-2025 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CoreData 30 | import Foundation 31 | 32 | /// `CoreDataContainer` is a subclass of `NSPersistentContainer` 33 | /// for creating and using a Core Data stack. 34 | /// 35 | /// The persistent store description that will be used to 36 | /// create/load the store is configured with the following 37 | /// defaults: 38 | /// 39 | /// - `shouldAddStoreAsynchronously`: true 40 | /// 41 | /// The following persistent store options are set by 42 | /// default: 43 | /// 44 | /// - `NSPersistentHistoryTrackingKey`: true 45 | /// 46 | /// If you want to change these defaults modify the store 47 | /// description before you load the store. 48 | @available(iOS 10.0, macOS 10.12, watchOS 3.0, tvOS 10.0, *) 49 | public class CoreDataContainer: NSPersistentContainer, @unchecked Sendable { 50 | /// Author used for the viewContext as an identifier 51 | /// in persistent history transactions. 52 | public var appTransactionAuthorName = "app" 53 | 54 | /// Default directory for the persistent stores 55 | /// - Returns: A `URL` for the directory containing the 56 | /// persistent store files. 57 | /// 58 | /// - Note: Adding the launch argument "-UNITTEST" to the 59 | /// scheme appends the directory "UNITTEST" to the 60 | /// default directory returned by `NSPersistentContainer`. 61 | override public class func defaultDirectoryURL() -> URL { 62 | if ProcessInfo.processInfo.arguments.contains("-UNITTEST") { 63 | return super.defaultDirectoryURL().appendingPathComponent( 64 | "UNITTEST", 65 | isDirectory: true 66 | ) 67 | } 68 | return super.defaultDirectoryURL() 69 | } 70 | 71 | /// Creates and returns a `CoreDataController` object. It creates the 72 | /// managed object model, persistent store coordinator and main managed 73 | /// object context but does not load the persistent store. 74 | /// 75 | /// The default container has a persistent store description 76 | /// configured with the following defaults: 77 | /// 78 | /// - `shouldAddStoreAsynchronously`: true 79 | /// 80 | /// The following persistent store options are set by 81 | /// default: 82 | /// 83 | /// - `NSPersistentHistoryTrackingKey`: true 84 | /// 85 | /// If you want to change these defaults modify the store 86 | /// description before you load the store. 87 | /// 88 | /// - Parameter name: The name of the persistent container. 89 | /// By default, this will also be used as the name of the 90 | /// managed object model and persistent store sql file. 91 | /// 92 | /// - Parameter bundle: An optional bundle to load the model(s) from. 93 | /// Default is `.main`. 94 | /// 95 | /// - Parameter url: A URL for the location of the persistent store. 96 | /// If not specified the store is created using the container name 97 | /// in the default container directory. Default is `nil`. 98 | /// 99 | /// - Parameter inMemory: Create the SQLite store in memory. 100 | /// Default is `false`. Using an in-memory store overrides 101 | /// the store url and sets `shouldAddStoreAsynchronously` to 102 | /// `false.` 103 | /// 104 | /// - Parameter shouldAddStoreAsynchronously: Load the store async. 105 | /// Default is `true`. This parameter is ignored for in-memory 106 | /// stores. 107 | public convenience init( 108 | name: String, 109 | bundle: Bundle = .main, 110 | url: URL? = nil, 111 | inMemory: Bool = false, 112 | shouldAddStoreAsynchronously: Bool = true 113 | ) { 114 | guard let momURL = bundle.url(forResource: name, withExtension: "momd") 115 | else { 116 | fatalError( 117 | "Unable to find \(name).momd in bundle \(bundle.bundleURL)" 118 | ) 119 | } 120 | 121 | guard let mom = NSManagedObjectModel(contentsOf: momURL) else { 122 | fatalError("Unable to create model from \(momURL)") 123 | } 124 | 125 | self.init(name: name, mom: mom, url: url, inMemory: inMemory, shouldAddStoreAsynchronously: shouldAddStoreAsynchronously) 126 | } 127 | 128 | /// Creates and returns a `CoreDataController` object with multiple 129 | /// persistent stores. 130 | /// 131 | /// The persistent store descriptions are created with a default 132 | /// configuration. This loads the store synchronously and does 133 | /// not enable history or remote change notifications. If you 134 | /// want to change the default configuration do it before 135 | /// loading the store(s). 136 | /// 137 | /// - Parameter name: The name of the persistent container. 138 | /// By default, this is used to name the persistent store 139 | /// sql file. 140 | /// 141 | /// - Parameter bundle: An optional bundle to load the model(s) from. 142 | /// Default is `.main`. 143 | /// 144 | /// - Parameter urls: One or more URLs for the location of the 145 | /// persistent stores. All stores are added to the container. 146 | /// - Parameter isReadOnly: Is the store read-only. Default is `false`. 147 | /// - Parameter shouldAddStoreAsynchronously: Load the store asynchronously. 148 | /// Default is `false`. 149 | /// - Parameter historyTracking: Enable persistent history tracking. 150 | /// Default is `false`. 151 | 152 | public init( 153 | name: String, 154 | bundle: Bundle = .main, 155 | urls: [URL], 156 | isReadOnly: Bool = false, 157 | shouldAddStoreAsynchronously: Bool = false, 158 | historyTracking: Bool = false 159 | ) { 160 | guard let momURL = bundle.url(forResource: name, withExtension: "momd") 161 | else { 162 | fatalError( 163 | "Unable to find \(name).momd in bundle \(bundle.bundleURL)" 164 | ) 165 | } 166 | 167 | guard let mom = NSManagedObjectModel(contentsOf: momURL) else { 168 | fatalError("Unable to create model from \(momURL)") 169 | } 170 | 171 | super.init(name: name, managedObjectModel: mom) 172 | 173 | let descriptions: [NSPersistentStoreDescription] = urls.map { 174 | let description = NSPersistentStoreDescription(url: $0) 175 | description.isReadOnly = isReadOnly 176 | description.shouldAddStoreAsynchronously = 177 | shouldAddStoreAsynchronously 178 | if historyTracking { 179 | description.setOption( 180 | true as NSNumber, 181 | forKey: NSPersistentHistoryTrackingKey 182 | ) 183 | } 184 | return description 185 | } 186 | 187 | self.persistentStoreDescriptions = descriptions 188 | } 189 | 190 | /// Creates and returns a `CoreDataController` object. It creates the 191 | /// persistent store coordinator and main managed object context but 192 | /// does not load the persistent store. 193 | /// 194 | /// - Parameter name: The name of the persistent container. 195 | /// By default, this is used to name the persistent store 196 | /// sql file. 197 | /// 198 | /// - Parameter mom: The managed object model. 199 | /// 200 | /// - Parameter url: A URL for the location of the persistent store. 201 | /// If not specified the store is created using the container name 202 | /// in the default container directory. Default is `nil`. 203 | /// 204 | /// - Parameter inMemory: Create the SQLite store in memory. 205 | /// Default is `false`. Using an in-memory store overrides 206 | /// the store url and sets `shouldAddStoreAsynchronously` to 207 | /// `false.` 208 | /// 209 | /// - Parameter shouldAddStoreAsynchronously: Load the store async. 210 | /// Default is `true`. This parameter is ignored for in-memory 211 | /// stores. 212 | public init( 213 | name: String, 214 | mom: NSManagedObjectModel, 215 | url: URL? = nil, 216 | inMemory: Bool = false, 217 | shouldAddStoreAsynchronously: Bool = true 218 | ) { 219 | super.init(name: name, managedObjectModel: mom) 220 | configureDefaults( 221 | url: url, 222 | inMemory: inMemory, 223 | shouldAddStoreAsynchronously: shouldAddStoreAsynchronously 224 | ) 225 | } 226 | 227 | /// The `URL` of the persistent store for this Core Data Stack. If there 228 | /// is more than one store this property returns the first store it finds. 229 | /// The store may not yet exist. It will be created at this URL by default 230 | /// when first loaded. 231 | 232 | public var storeURL: URL? { 233 | guard let firstDescription = persistentStoreDescriptions.first else { 234 | return nil 235 | } 236 | return firstDescription.url 237 | } 238 | 239 | /// A read-only flag indicating if the persistent store is loaded. 240 | public private(set) var isStoreLoaded = false 241 | 242 | /// Load the persistent store. 243 | /// 244 | /// Call this method after creating the container to load the store. 245 | /// After the store has been loaded, the view context is configured 246 | /// as follows: 247 | /// 248 | /// - `automaticallyMergesChangesFromParent`: `true` 249 | /// - `name`: `viewContext` 250 | /// - `mergePolicy`: `NSMergeByPropertyObjectTrumpMergePolicy` 251 | /// - `transactionAuthor`: see `appTransactionAuthorName`. 252 | /// 253 | /// The query generation is also pinned to the current generation 254 | /// (unless this is an in-memory store). 255 | /// 256 | /// - Parameter block: This handler block is executed on the calling 257 | /// thread when the loading of the persistent store has completed. 258 | 259 | override public func loadPersistentStores( 260 | completionHandler block: @escaping ( 261 | NSPersistentStoreDescription, Error? 262 | ) -> Void 263 | ) { 264 | super.loadPersistentStores { storeDescription, error in 265 | var completionError: Error? = error 266 | if error == nil { 267 | self.isStoreLoaded = true 268 | self.viewContext.automaticallyMergesChangesFromParent = true 269 | self.viewContext.mergePolicy = 270 | NSMergePolicy.mergeByPropertyObjectTrump 271 | self.viewContext.name = "viewContext" 272 | self.viewContext.transactionAuthor = 273 | self.appTransactionAuthorName 274 | 275 | // Pin the view context to the current query generation. 276 | // This is not supported for an in-memory store 277 | if storeDescription.url != URL(fileURLWithPath: "/dev/null") { 278 | if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, 279 | *) { 280 | completionError = self.pin(self.viewContext) 281 | } 282 | } 283 | } 284 | block(storeDescription, completionError) 285 | } 286 | } 287 | 288 | /// Returns a new managed object context that executes 289 | /// on a private queue. 290 | /// 291 | /// The merge policy is property object trump 292 | /// and the undo manager is disabled. 293 | public override func newBackgroundContext() -> NSManagedObjectContext { 294 | let context = super.newBackgroundContext() 295 | context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump 296 | context.undoManager = nil 297 | return context 298 | } 299 | 300 | private func configureDefaults( 301 | url: URL?, 302 | inMemory: Bool, 303 | shouldAddStoreAsynchronously: Bool = true 304 | ) { 305 | if let storeDescription = persistentStoreDescriptions.first { 306 | storeDescription.shouldAddStoreAsynchronously = 307 | shouldAddStoreAsynchronously 308 | storeDescription.setOption( 309 | true as NSNumber, 310 | forKey: NSPersistentHistoryTrackingKey 311 | ) 312 | if let url { 313 | storeDescription.url = url 314 | } 315 | if inMemory { 316 | storeDescription.url = URL(fileURLWithPath: "/dev/null") 317 | storeDescription.shouldAddStoreAsynchronously = false 318 | } 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /Sources/CoreDataKit/CoreDataCloudKitContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2022-2025 Keith Harrison. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // 1. Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 13 | // 3. Neither the name of the copyright holder nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | // POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import CloudKit 30 | import CoreData 31 | import Foundation 32 | 33 | /// `CoreDataCloudKitContainer` is a subclass of `NSPersistentCloudKitContainer` 34 | /// for creating and using a Core Data stack that syncs with CloudKit. 35 | /// 36 | /// CoreDataCloudKitContainer is a subclass of NSPersistentCloudKitContainer 37 | /// capable of managing both CloudKit-backed and non-cloud stores. 38 | /// 39 | /// If you have a CloudKit container identifier in the app's 40 | /// entitlements the first identifier is used to set the 41 | /// cloudKitContainerOptions identifier of the first 42 | /// persistent store description. Otherwise, the store is local. 43 | /// 44 | /// The persistent store description that will be used to 45 | /// create/load the store is configured with the following 46 | /// defaults: 47 | /// 48 | /// - `shouldAddStoreAsynchronously`: true 49 | /// 50 | /// The following persistent store options are set by 51 | /// default: 52 | /// 53 | /// - `NSPersistentHistoryTrackingKey`: true 54 | /// - `NSPersistentStoreRemoteChangeNotificationPostOptionKey`: true 55 | /// 56 | /// If you want to change these defaults modify the store 57 | /// description before you load the store. 58 | @available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) 59 | public class CoreDataCloudKitContainer: NSPersistentCloudKitContainer, @unchecked Sendable { 60 | /// Author used for the viewContext as an identifier 61 | /// in persistent history transactions. 62 | public var appTransactionAuthorName = "app" 63 | 64 | /// Disable CloudKit sync. Default is false. 65 | /// 66 | /// If this property is true any cloudKitContainerOptions 67 | /// in the store descriptions are removed before the 68 | /// store is loaded. 69 | /// 70 | /// - Note: This property can only be set when creating 71 | /// the container. Once a store is loaded there is no 72 | /// supported mechanism to disable sync, other than 73 | /// unloading/removing the store. 74 | private let syncDisabled: Bool 75 | 76 | /// Default directory for the persistent stores 77 | /// - Returns: A `URL` for the directory containing the 78 | /// persistent store files. 79 | /// 80 | /// - Note: Adding the launch argument "-UNITTEST" to the 81 | /// scheme appends the directory "UNITTEST" to the 82 | /// default directory returned by `NSPersistentContainer`. 83 | override public class func defaultDirectoryURL() -> URL { 84 | if ProcessInfo.processInfo.arguments.contains("-UNITTEST") { 85 | return super.defaultDirectoryURL().appendingPathComponent("UNITTEST", isDirectory: true) 86 | } 87 | return super.defaultDirectoryURL() 88 | } 89 | 90 | /// Creates and returns a `CoreDataCloudKitController` object. It creates the 91 | /// managed object model, persistent store coordinator and main managed 92 | /// object context but does not load the persistent store. 93 | /// 94 | /// The default container has a persistent store description 95 | /// configured with the following defaults: 96 | /// 97 | /// - `shouldAddStoreAsynchronously`: true 98 | /// 99 | /// The following persistent store options are set by 100 | /// default: 101 | /// 102 | /// - `NSPersistentHistoryTrackingKey`: true 103 | /// - `NSPersistentStoreRemoteChangeNotificationPostOptionKey`: true 104 | /// 105 | /// If you have a CloudKit container identifier in the app's 106 | /// entitlements the first identifier is used to set the 107 | /// cloudKitContainerOptions identifier of the first 108 | /// persistent store description. Otherwise, the store is local. 109 | /// 110 | /// If you want to change these defaults modify the store 111 | /// description before you load the store. 112 | /// 113 | /// - Parameter name: The name of the persistent container. 114 | /// By default, this will also be used as the name of the 115 | /// managed object model and persistent store sql file. 116 | /// 117 | /// - Parameter bundle: An optional bundle to load the model(s) from. 118 | /// Default is `.main`. 119 | /// 120 | /// - Parameter url: A URL for the location of the persistent store. 121 | /// If not specified the store is created using the container name 122 | /// in the default container directory. Default is `nil`. 123 | /// 124 | /// - Parameter inMemory: Create the SQLite store in memory. 125 | /// Default is `false`. Using an in-memory store overrides 126 | /// the store url and sets `shouldAddStoreAsynchronously` to 127 | /// `false.` 128 | /// 129 | /// - Parameter shouldAddStoreAsynchronously: Load the store async. 130 | /// Default is `true`. This parameter is ignored for in-memory 131 | /// stores. 132 | /// 133 | /// - Parameter syncDisabled: Disable CloudKit sync. 134 | /// Default is `false`. When `true` any cloudKitContainerOptions 135 | /// set on the store description are removed before loading the 136 | /// store. Use this for SwiftUI previews and testing. 137 | public convenience init(name: String, bundle: Bundle = .main, url: URL? = nil, inMemory: Bool = false, shouldAddStoreAsynchronously: Bool = true, syncDisabled: Bool = false) { 138 | guard let momURL = bundle.url(forResource: name, withExtension: "momd") else { 139 | fatalError("Unable to find \(name).momd in bundle \(bundle.bundleURL)") 140 | } 141 | 142 | guard let mom = NSManagedObjectModel(contentsOf: momURL) else { 143 | fatalError("Unable to create model from \(momURL)") 144 | } 145 | 146 | self.init(name: name, mom: mom, url: url, inMemory: inMemory, shouldAddStoreAsynchronously: shouldAddStoreAsynchronously, syncDisabled: syncDisabled) 147 | } 148 | 149 | /// Creates and returns a `CoreDataCloudKitController` object. It creates the 150 | /// persistent store coordinator and main managed object context but 151 | /// does not load the persistent store. 152 | /// 153 | /// The default container has a persistent store description 154 | /// configured with the following defaults: 155 | /// 156 | /// - `shouldAddStoreAsynchronously`: true 157 | /// 158 | /// The following persistent store options are set by 159 | /// default: 160 | /// 161 | /// - `NSPersistentHistoryTrackingKey`: true 162 | /// - `NSPersistentStoreRemoteChangeNotificationPostOptionKey`: true 163 | /// 164 | /// If you have a CloudKit container identifier in the app's 165 | /// entitlements the first identifier is used to set the 166 | /// cloudKitContainerOptions identifier of the first 167 | /// persistent store description. Otherwise, the store is local. 168 | /// 169 | /// If you want to change these defaults modify the store 170 | /// description before you load the store. 171 | /// 172 | /// - Parameter name: The name of the persistent container. 173 | /// By default, this is used to name the persistent store 174 | /// sql file. 175 | /// 176 | /// - Parameter mom: The managed object model. 177 | /// 178 | /// - Parameter url: A URL for the location of the persistent store. 179 | /// If not specified the store is created using the container name 180 | /// in the default container directory. Default is `nil`. 181 | /// 182 | /// - Parameter inMemory: Create the SQLite store in memory. 183 | /// Default is `false`. Using an in-memory store overrides 184 | /// the store url and sets `shouldAddStoreAsynchronously` to 185 | /// `false.` 186 | /// 187 | /// - Parameter shouldAddStoreAsynchronously: Load the store async. 188 | /// Default is `true`. This parameter is ignored for in-memory 189 | /// stores. 190 | /// 191 | /// - Parameter syncDisabled: Disable CloudKit sync. 192 | /// Default is `false`. When `true` any cloudKitContainerOptions 193 | /// set on the store description are removed before loading the 194 | /// store. Use this for SwiftUI previews and testing. 195 | public init(name: String, mom: NSManagedObjectModel, url: URL? = nil, inMemory: Bool = false, shouldAddStoreAsynchronously: Bool = true, syncDisabled: Bool = false) { 196 | self.syncDisabled = syncDisabled 197 | super.init(name: name, managedObjectModel: mom) 198 | configureDefaults(url: url, inMemory: inMemory, shouldAddStoreAsynchronously: shouldAddStoreAsynchronously) 199 | } 200 | 201 | /// The `URL` of the persistent store for this Core Data Stack. If there 202 | /// is more than one store this property returns the first store it finds. 203 | /// The store may not yet exist. It will be created at this URL by default 204 | /// when first loaded. 205 | public var storeURL: URL? { 206 | guard let firstDescription = persistentStoreDescriptions.first else { 207 | return nil 208 | } 209 | return firstDescription.url 210 | } 211 | 212 | /// Load the persistent store. 213 | /// 214 | /// Call this method after creating the container to load the store. 215 | /// After the store has been loaded, the view context is configured 216 | /// as follows: 217 | /// 218 | /// - `automaticallyMergesChangesFromParent`: `true` 219 | /// - `name`: `viewContext` 220 | /// - `mergePolicy`: `NSMergeByPropertyObjectTrumpMergePolicy` 221 | /// - `transactionAuthor`: see `appTransactionAuthorName`. 222 | /// 223 | /// The query generation is also pinned to the current generation 224 | /// (unless this is an in-memory store). 225 | /// 226 | /// - Parameter block: This handler block is executed on the calling 227 | /// thread when the loading of the persistent store has completed. 228 | override public func loadPersistentStores(completionHandler block: @escaping (NSPersistentStoreDescription, Error?) -> Void) { 229 | overrideStoreDescriptions(syncDisabled: syncDisabled) 230 | super.loadPersistentStores { storeDescription, error in 231 | var completionError: Error? = error 232 | if error == nil { 233 | self.viewContext.automaticallyMergesChangesFromParent = true 234 | self.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump 235 | self.viewContext.name = "viewContext" 236 | self.viewContext.transactionAuthor = self.appTransactionAuthorName 237 | 238 | // Pin the view context to the current query generation. 239 | // This is not supported for an in-memory store 240 | if storeDescription.url != URL(fileURLWithPath: "/dev/null") { 241 | if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) { 242 | completionError = self.pin(self.viewContext) 243 | } 244 | } 245 | } 246 | block(storeDescription, completionError) 247 | } 248 | } 249 | 250 | /// Returns a new managed object context that executes 251 | /// on a private queue. 252 | /// 253 | /// The merge policy is property object trump 254 | /// and the undo manager is disabled. 255 | public override func newBackgroundContext() -> NSManagedObjectContext { 256 | let context = super.newBackgroundContext() 257 | context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump 258 | context.undoManager = nil 259 | return context 260 | } 261 | 262 | private func configureDefaults(url: URL?, inMemory: Bool, shouldAddStoreAsynchronously: Bool = true) { 263 | if let storeDescription = persistentStoreDescriptions.first { 264 | storeDescription.shouldAddStoreAsynchronously = shouldAddStoreAsynchronously 265 | storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) 266 | storeDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) 267 | if let url { 268 | storeDescription.url = url 269 | } 270 | if inMemory { 271 | storeDescription.url = URL(fileURLWithPath: "/dev/null") 272 | storeDescription.shouldAddStoreAsynchronously = false 273 | } 274 | } 275 | } 276 | 277 | private func overrideStoreDescriptions(syncDisabled: Bool) { 278 | if syncDisabled == false { return } 279 | for storeDescription in persistentStoreDescriptions { 280 | storeDescription.cloudKitContainerOptions = nil 281 | } 282 | } 283 | } 284 | --------------------------------------------------------------------------------