├── .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 |
--------------------------------------------------------------------------------