From 145edd5a7de8dddc889233fb473f44dcc1d22995 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Mon, 21 Oct 2019 21:15:46 +0900 Subject: [PATCH] README update --- .../xcschemes/CoreStore iOS.xcscheme | 2 +- README.md | 152 ++++++++++++------ ...reStore+CustomDebugStringConvertible.swift | 44 ----- Sources/CoreStore+Migration.swift | 32 ---- Sources/CoreStore+Setup.swift | 26 --- Sources/DataStack+Migration.swift | 136 ---------------- Sources/DataStack.swift | 93 ----------- ...fableDataSource.CollectionView-UIKit.swift | 4 +- .../DiffableDataSource.TableView-UIKit.swift | 3 + ...s.DiffableDataUIDispatcher.Changeset.swift | 1 + ....DiffableDataUIDispatcher.DiffResult.swift | 13 +- ...ableDataUIDispatcher.StagedChangeset.swift | 3 +- ...edDiffableDataSourceSnapshotDelegate.swift | 31 +--- Sources/StorageInterface.swift | 105 ------------ ...ableDataSource.CollectionView-AppKit.swift | 4 +- 15 files changed, 129 insertions(+), 520 deletions(-) diff --git a/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore iOS.xcscheme b/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore iOS.xcscheme index fdef8e3..01ffb76 100644 --- a/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore iOS.xcscheme +++ b/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore iOS.xcscheme @@ -71,7 +71,7 @@ (.xcdatamodeld file) | `DataStack` | -| `NSPersistentStore`
("Configuration"s in the .xcdatamodeld file) | `StorageInterface` implementations
(`InMemoryStore`, `SQLiteStore`, `ICloudStore`) | +| `NSPersistentContainer`
(.xcdatamodeld file) | `DataStack` | +| `NSPersistentStoreDescription`
("Configuration"s in the .xcdatamodeld file) | `StorageInterface` implementations
(`InMemoryStore`, `SQLiteStore`) | | `NSManagedObjectContext` | `BaseDataTransaction` subclasses
(`SynchronousDataTransaction`, `AsynchronousDataTransaction`, `UnsafeDataTransaction`) | A lot of Core Data wrapper libraries set up their `NSManagedObjectContext`s this way: nested contexts -Nesting saves from child context to the root context ensures maximum data integrity between contexts without blocking the main queue. But in reality, merging contexts is still by far faster than saving contexts. CoreStore's `DataStack` takes the best of both worlds by treating the main `NSManagedObjectContext` as a read-only context, and only allows changes to be made within *transactions* on the child context: +Nesting saves from child context to the root context ensures maximum data integrity between contexts without blocking the main queue. But in reality, merging contexts is still by far faster than saving contexts. CoreStore's `DataStack` takes the best of both worlds by treating the main `NSManagedObjectContext` as a read-only context (or "viewContext"), and only allows changes to be made within *transactions* on the child context: nested contexts and merge hybrid @@ -242,7 +244,7 @@ class MyViewController: UIViewController { } } func methodToBeCalledLaterOn() { - let objects = self.dataStack.fetchAll(From(MyEntity)) + let objects = self.dataStack.fetchAll(From()) print(objects) } } @@ -250,7 +252,7 @@ class MyViewController: UIViewController { > 💡By default, CoreStore will initialize `NSManagedObject`s from *.xcdatamodeld* files, but you can create models completely from source code using `CoreStoreObject`s and `CoreStoreSchema`. To use this feature, refer to [Type-safe `CoreStoreObject`s](#type-safe-corestoreobjects). -Notice that in our previous examples, `addStorageAndWait(_:)` and `addStorage(_:completion:)` both accept either `InMemoryStore`, `SQLiteStore`, or `ICloudStore`. These implement the `StorageInterface` protocol. +Notice that in our previous examples, `addStorageAndWait(_:)` and `addStorage(_:completion:)` both accept either `InMemoryStore`, or `SQLiteStore`. These implement the `StorageInterface` protocol. ### In-memory store The most basic `StorageInterface` concrete type is the `InMemoryStore`, which just stores objects in memory. Since `InMemoryStore`s always start with a fresh empty data, they do not need any migration information. @@ -298,16 +300,7 @@ If you have custom `NSIncrementalStore` or `NSAtomicStore` subclasses, you can i ## Migrations ### Declaring model versions -Until CoreStore 4.0, model versions were always assumed to be declared in *.xcdatamodeld* files. The `DataStack` loads these for us by accepting the *.xcdatamodeld* file name and the `Bundle` where the files can be found: -```swift -CoreStoreDefaults.dataStack = DataStack( - xcodeModelName: "MyModel", - bundle: Bundle.main, - migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"] -) -``` - -Starting CoreStore 4.0, model versions are now expressed as a first-class protocol, `DynamicSchema`. CoreStore currently supports the following schema classes: +Model versions are now expressed as a first-class protocol, `DynamicSchema`. CoreStore currently supports the following schema classes: - **`XcodeDataModelSchema`**: a model version with entities loaded from a *.xcdatamodeld* file. - **`CoreStoreSchema`**: a model version created with `CoreStoreObject` entities. *(See [Type-safe `CoreStoreObject`s](#type-safe-corestore-objects))* - **`UnsafeDataModelSchema`**: a model version created with an existing `NSManagedObjectModel` instance. @@ -432,7 +425,7 @@ CoreStoreDefaults.dataStack = DataStack( ### Starting migrations -We have seen `addStorageAndWait(...)` used to initialize our persistent store. As the method name's *~AndWait* suffix suggests though, this method blocks so it should not do long tasks such as store migrations. In fact CoreStore will only attempt a synchronous **lightweight** migration if you explicitly provide the `.allowSynchronousLightweightMigration` option: +We have seen `addStorageAndWait(...)` used to initialize our persistent store. As the method name's *~AndWait* suffix suggests though, this method blocks so it should not do long tasks such as data migrations. In fact CoreStore will only attempt a synchronous **lightweight** migration if you explicitly provide the `.allowSynchronousLightweightMigration` option: ```swift try dataStack.addStorageAndWait( SQLiteStore( @@ -542,11 +535,11 @@ Each `MigrationType` indicates the migration type for each step in the `Migratio ### Custom migrations -Before CoreStore 4.0, the only way to implement custom migrations is to use Core Data's standard method: declaring entity mappings through *.xcmappingmodel* files. Starting CoreStore 4.0, new ways to declare migration mappings have been added: +CoreStore offers several ways to declare migration mappings: -- `InferredSchemaMappingProvider`: The default mapping provider which tries to infer model migration between two `DynamicSchema` versions either by searching all *.xcmappingmodel* files from `Bundle.allBundles`, or by relying on lightweight migration if possible. -- `XcodeSchemaMappingProvider`: A mapping provider which loads entity mappings from *.xcmappingmodel* files in a specified `Bundle`. - `CustomSchemaMappingProvider`: A mapping provider that infers mapping initially, but also accepts custom mappings for specified entities. This was added to support custom migrations with `CoreStoreObject`s as well, but may also be used with `NSManagedObject`s. +- `XcodeSchemaMappingProvider`: A mapping provider which loads entity mappings from *.xcmappingmodel* files in a specified `Bundle`. +- `InferredSchemaMappingProvider`: The default mapping provider which tries to infer model migration between two `DynamicSchema` versions either by searching all *.xcmappingmodel* files from `Bundle.allBundles`, or by relying on lightweight migration if possible. These mapping providers conform to `SchemaMappingProvider` and can be passed to `SQLiteStore`'s initializer: ```swift @@ -613,18 +606,7 @@ dataStack.perform( } ) ``` -or for the default stack, directly from `CoreStore`: -```swift -dataStack.perform( - asynchronous: { (transaction) -> Void in - // make changes - }, - completion: { (result) -> Void in - // ... - } -) -``` -Transaction blocks automatically save changes once the block completes. To cancel and rollback a transaction, throw a `CoreStoreError.userCancelled` from inside the closure by calling `try transaction.cancel()`: +Transaction closures automatically save changes once the closures completes. To cancel and rollback a transaction, throw a `CoreStoreError.userCancelled` from inside the closure by calling `try transaction.cancel()`: ```swift dataStack.perform( asynchronous: { (transaction) -> Void in @@ -907,7 +889,7 @@ dataStack.perform( ## Importing data -Some times, if not most of the time, the data that we save to Core Data comes from external sources such as web servers or external files. If you have for example a JSON dictionary, you may be extracting values as such: +Some times, if not most of the time, the data that we save to Core Data comes from external sources such as web servers or external files. If you have a JSON dictionary for example, you may be extracting values as such: ```swift let json: [String: Any] = // ... person.name = json["name"] as? NSString @@ -1089,7 +1071,7 @@ dataStack.perform( completion: { _ in } ) ``` -As with `ImportableObject`, you can control wether to skip importing an object by implementing +As with `ImportableObject`, you can control whether to skip importing an object by implementing `shouldInsert(from:in:)` and `shouldUpdate(from:in:)`, or to cancel all objects by `throw`ing an error from the `uniqueID(from:in:)`, `didInsert(from:in:)` or `update(from:in:)` methods. @@ -1154,7 +1136,7 @@ var people = dataStack.fetchAll( Where(predicate) // predicate initializer ) ``` -⭐️Starting CoreStore 5.0, `Where` clauses became more type-safe and are now generic types. To avoid verbose repetition of the generic object type, fetch methods now support **Fetch Chain builders**. We can also use Swift's Smart KeyPaths as the `Where` clause expression: + `Where` clauses are generic types. To avoid verbose repetition of the generic object type, fetch methods support **Fetch Chain builders**. We can also use Swift's Smart KeyPaths as the `Where` clause expression: ```swift var people = try dataStack.fetchAll( From() @@ -1180,7 +1162,7 @@ var mostValuablePeople = try dataStack.fetchAll( ) ``` As seen above, `OrderBy` accepts a list of `SortKey` enumeration values, which can be either `.ascending` or `.descending`. -⭐️As with `Where` clauses, CoreStore 5.0 turned `OrderBy` clauses into generic types. To avoid verbose repetition of the generic object type, fetch methods now support **Fetch Chain builders**. We can also use Swift's Smart KeyPaths as the `OrderBy` clause expression: +As with `Where` clauses, `OrderBy` clauses are also generic types. To avoid verbose repetition of the generic object type, fetch methods support **Fetch Chain builders**. We can also use Swift's Smart KeyPaths as the `OrderBy` clause expression: ```swift var people = try dataStack.fetchAll( From() @@ -1251,7 +1233,7 @@ The `Select` clause specifies the target attribute/aggregate key, as well as let johnsAge = try dataStack.queryValue( From(), Select("age"), - Where("name == %@", "John Smith") + Where("name == %@", "John Smith") ) ``` The example above queries the "age" property for the first object that matches the `Where` condition. `johnsAge` will be bound to type `Int?`, as indicated by the `Select` generic type. For `queryValue(...)`, types that conform to `QueryableAttributeType` are allowed as the return type (and therefore as the generic type for `Select`). @@ -1263,7 +1245,7 @@ let allAges = try dataStack.queryAttributes( Select("age") ) ``` -⭐️Starting CoreStore 5.0, query methods now support **Query Chain builders**. We can also use Swift's Smart KeyPaths to use in the expressions: +query methods also support **Query Chain builders**. We can also use Swift's Smart KeyPaths to use in the expressions: ```swift let johnsAge = try dataStack.queryValue( From() @@ -1357,7 +1339,7 @@ let personJSON = try dataStack.queryAttributes( GroupBy("age") ) ``` -⭐️Starting CoreStore 5.0, `GroupBy` clauses are now also generic types and now support **Query Chain builders**. We can also use Swift's Smart KeyPaths to use in the expressions: +`GroupBy` clauses are also generic types and support **Query Chain builders**. We can also use Swift's Smart KeyPaths to use in the expressions: ```swift let personJSON = try dataStack.queryAttributes( From() @@ -1401,8 +1383,7 @@ Take special care when implementing `CoreStoreLogger`'s `assert(...)` and `abort - `assert(...)`: The behavior between `DEBUG` and release builds, or `-O` and `-Onone`, are all left to the implementers' responsibility. CoreStore calls `CoreStoreLogger.assert(...)` only for invalid but usually recoverable errors (for example, early validation failures that may cause an error thrown and handled somewhere else) - `abort(...)`: This method is *the* last-chance for your app to *synchronously* log a fatal error within CoreStore. The app will be terminated right after this function is called (CoreStore calls `fatalError()` internally) -Starting CoreStore 2.0, all CoreStore types now have very useful (and pretty formatted!) `print(...)` outputs. - +All CoreStore types have very useful (and pretty formatted!) `print(...)` outputs. A couple of examples, `ListMonitor`: screen shot 2016-07-10 at 22 56 44 @@ -1417,8 +1398,15 @@ These are all implemented with `CustomDebugStringConvertible.debugDescription`, CoreStore provides type-safe wrappers for observing managed objects: -- `ObjectMonitor`: use to monitor changes to a single `NSManagedObject` or `CoreStoreObject` instance (instead of Key-Value Observing) -- `ListMonitor`: use to monitor changes to a list of `NSManagedObject` or `CoreStoreObject` instances (instead of `NSFetchedResultsController`) +| | *ListMonitor* | 🆕*ListPublisher* | *ObjectMonitor* | 🆕*ObjectPublisher* | +| --- | --- | --- | --- | --- | +| *Number of objects* | N | N | 1 | 1 | +| *Allows multiple observers* | ✅ | ✅ | ✅ | ✅ | +| *Emits fine-grained changes* | ✅ | ❌ | ✅ | ❌ | +| *Emits DiffableDataSource snapshots* | ❌ | ✅ | ❌ | ✅ | +| *Closure callback* | ❌ | ✅ | ❌ | ✅ | +| *Delegate methods* | ✅ | ❌ | ✅ | ❌ | +| *SwiftUI support* | ❌ | ✅ | ❌ | ✅ | ### Observe a single property To get notifications for single property changes in an object, there are two methods depending on the object's base class. @@ -1439,10 +1427,35 @@ let observer = person.age.observe(options: [.new]) { (person, change) For both methods, you will need to keep a reference to the returned `observer` for the duration of the observation. +### Observe a single object's updates -### Observe a single object +Observers of an `ObjectPublisher` can receive notifications if any of the object's property changes. You can create an `ObjectPublisher` from the object directly: +```swift +let objectPublisher: ObjectPublisher = person.asPublisher(in: dataStack) +``` +or by indexing a `ListPublisher`'s `ListSnapshot`: +```swift +let listPublisher: ListPublisher = // ... +// ... +let objectPublisher = listPublisher.snapshot[indexPath] +``` +(See [`ListPublisher` examples](#observe-a-diffable-list) below) -To observe an object itself as a whole, implement the `ObjectObserver` protocol and specify the `EntityType`: +To receive notifications, call the `ObjectPublisher`'s `addObserve(...)` method passing the owner of the callback closure: +```swift +objectPublisher.addObserver(self) { [weak self] (objectPublisher) in + let snapshot: ObjectSnapshot = objectPublisher.snapshot + // handle changes +} +``` +Note that the owner instance will not be retained. You may call `ObjectPublisher.removeObserver(...)` explicitly to stop receiving notifications, but the `ObjectPublisher` also discontinues sending events to deallocated observers. + +The `ObjectSnapshot` returned from the `ObjectPublisher.snapshot` property returns a full-copy `struct` of all properties of the object. This is ideal for managing states as they are thread-safe and are not affected by further changes to the actual object. `ObjectPublisher` automatically updates its `snapshot` value to the latest state of the object. + + +### Observe a single object's per-property updates + +If you need to track specifically which properties change in an object, implement the `ObjectObserver` protocol and specify the `EntityType`: ```swift class MyViewController: UIViewController, ObjectObserver { func objectMonitor(monitor: ObjectMonitor, willUpdateObject object: MyPersonEntity) { @@ -1470,8 +1483,51 @@ You can get `ObjectMonitor`'s object through its `object` property. If the objec While `ObjectMonitor` exposes `removeObserver(...)` as well, it only stores `weak` references of the observers and will safely unregister deallocated observers. -### Observe a list of objects -To observe a list of objects, implement one of the `ListObserver` protocols and specify the `EntityType`: +### Observe a diffable list + +Observers of a `ListPublisher` can receive notifications whenever its fetched result set changes. You can create a `ListPublisher` by fetching from the `DataStack`: +```swift +let listPublisher = dataStack.listPublisher( + From() + .sectionBy(\.age") { "Age \($0)" } // sections are optional + .where(\.title == "Engineer") + .orderBy(.ascending(\.lastName)) +) +``` +To receive notifications, call the `ListPublisher`'s `addObserve(...)` method passing the owner of the callback closure: +```swift +listPublisher.addObserver(self) { [weak self] (listPublisher) in + let snapshot: ListSnapshot = listPublisher.snapshot + // handle changes +} +``` +Note that the owner instance will not be retained. You may call `ListPublisher.removeObserver(...)` explicitly to stop receiving notifications, but the `ListPublisher` also discontinues sending events to deallocated observers. + +The `ListSnapshot` returned from the `ListPublisher.snapshot` property returns a full-copy `struct` of all sections and `NSManagedObject` items in the list. This is ideal for managing states as they are thread-safe and are not affected by further changes to the result set. `ListPublisher` automatically updates its `snapshot` value to the latest state of the fetch. + +Unlike `ListMonitor`s (See [`ListMonitor` examples](#observe-detailed-list-changes) below), a `ListPublisher` does not track detailed inserts, deletes, and moves. In return, a `ListPublisher` is a lot more lightweight and are designed to work well with `DiffableDataSource.TableView`s and `DiffableDataSource.CollectionView`s: +```swift +self.dataSource = DiffableDataSource.CollectionView( + collectionView: self.collectionView, + dataStack: CoreStoreDefaults.dataStack, + cellProvider: { (collectionView, indexPath, person) in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell + cell.setPerson(person) + return cell + } +) + +// ... + +listPublisher.addObserver(self) { [weak self] (listPublisher) in + self?.dataSource?.apply( + listPublisher.snapshot, animatingDifferences: true + ) +} +``` + +### Observe detailed list changes +If you need to track each object's inserts, deletes, moves, and updates, implement one of the `ListObserver` protocols and specify the `EntityType`: ```swift class MyViewController: UIViewController, ListObserver { func listMonitorDidChange(monitor: ListMonitor) { diff --git a/Sources/CoreStore+CustomDebugStringConvertible.swift b/Sources/CoreStore+CustomDebugStringConvertible.swift index 4502f4b..a6fab80 100644 --- a/Sources/CoreStore+CustomDebugStringConvertible.swift +++ b/Sources/CoreStore+CustomDebugStringConvertible.swift @@ -54,50 +54,6 @@ extension AsynchronousDataTransaction: CustomDebugStringConvertible, CoreStoreDe } -// MARK: - CloudStorageOptions - -extension CloudStorageOptions: CustomDebugStringConvertible, CoreStoreDebugStringConvertible { - - // MARK: CustomDebugStringConvertible - - public var debugDescription: String { - - return formattedDebugDescription(self) - } - - - // MARK: CoreStoreDebugStringConvertible - - public var coreStoreDumpString: String { - - var flags = [String]() - if self.contains(.recreateLocalStoreOnModelMismatch) { - - flags.append(".recreateLocalStoreOnModelMismatch") - } - if self.contains(.allowSynchronousLightweightMigration) { - - flags.append(".allowSynchronousLightweightMigration") - } - switch flags.count { - - case 0: - return "[.none]" - - case 1: - return "[.\(flags[0])]" - - default: - var string = "[\n" - string.append(flags.joined(separator: ",\n")) - string.indent(1) - string.append("\n]") - return string - } - } -} - - // MARK: - CoreStoreError extension CoreStoreError: CustomDebugStringConvertible, CoreStoreDebugStringConvertible { diff --git a/Sources/CoreStore+Migration.swift b/Sources/CoreStore+Migration.swift index e0dac94..dd31d5d 100644 --- a/Sources/CoreStore+Migration.swift +++ b/Sources/CoreStore+Migration.swift @@ -74,38 +74,6 @@ extension CoreStore { return CoreStoreDefaults.dataStack.addStorage(storage, completion: completion) } - - /** - Asynchronously adds a `CloudStorage` to the `CoreStoreDefaults.dataStack`. Migrations are also initiated by default. - ``` - guard let storage = ICloudStore( - ubiquitousContentName: "MyAppCloudData", - ubiquitousContentTransactionLogsSubdirectory: "logs/config1", - ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername", - ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0", - configuration: "Config1", - cloudStorageOptions: .recreateLocalStoreOnModelMismatch - ) else { - // iCloud is not available on the device - return - } - let migrationProgress = dataStack.addStorage( - storage, - completion: { result in - switch result { - case .success(let storage): // ... - case .failure(let error): // ... - } - } - ) - ``` - - parameter storage: the cloud storage - - parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `SetupResult` argument indicates the result. Note that the `CloudStorage` associated to the `SetupResult.success` may not always be the same instance as the parameter argument if a previous `CloudStorage` was already added at the same URL and with the same configuration. - */ - public static func addStorage(_ storage: T, completion: @escaping (SetupResult) -> Void) { - - CoreStoreDefaults.dataStack.addStorage(storage, completion: completion) - } /** Migrates a local storage to match the `CoreStoreDefaults.dataStack`'s managed object model version. This method does NOT add the migrated store to the data stack. diff --git a/Sources/CoreStore+Setup.swift b/Sources/CoreStore+Setup.swift index 4db95f0..b55260f 100644 --- a/Sources/CoreStore+Setup.swift +++ b/Sources/CoreStore+Setup.swift @@ -114,30 +114,4 @@ extension CoreStore { return try CoreStoreDefaults.dataStack.addStorageAndWait(storage) } - - /** - Adds a `CloudStorage` to the `CoreStoreDefaults.dataStack` and blocks until completion. - ``` - guard let storage = ICloudStore( - ubiquitousContentName: "MyAppCloudData", - ubiquitousContentTransactionLogsSubdirectory: "logs/config1", - ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername", - ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0", - configuration: "Config1", - cloudStorageOptions: .recreateLocalStoreOnModelMismatch - ) else { - // iCloud is not available on the device - return - } - try CoreStore.addStorageAndWait(storage) - ``` - - parameter storage: the local storage - - throws: a `CoreStoreError` value indicating the failure - - returns: the cloud storage added to the stack. Note that this may not always be the same instance as the parameter argument if a previous `CloudStorage` was already added at the same URL and with the same configuration. - */ - @discardableResult - public static func addStorageAndWait(_ storage: T) throws -> T { - - return try CoreStoreDefaults.dataStack.addStorageAndWait(storage) - } } diff --git a/Sources/DataStack+Migration.swift b/Sources/DataStack+Migration.swift index ab2eb75..8ffe463 100644 --- a/Sources/DataStack+Migration.swift +++ b/Sources/DataStack+Migration.swift @@ -248,142 +248,6 @@ extension DataStack { } } - /** - Asynchronously adds a `CloudStorage` to the stack. Migrations are also initiated by default. - ``` - guard let storage = ICloudStore( - ubiquitousContentName: "MyAppCloudData", - ubiquitousContentTransactionLogsSubdirectory: "logs/config1", - ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername", - ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0", - configuration: "Config1", - cloudStorageOptions: .recreateLocalStoreOnModelMismatch - ) else { - // iCloud is not available on the device - return - } - dataStack.addStorage( - storage, - completion: { result in - switch result { - case .success(let storage): // ... - case .failure(let error): // ... - } - } - ) - ``` - - parameter storage: the cloud storage - - parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `SetupResult` argument indicates the result. Note that the `CloudStorage` associated to the `SetupResult.success` may not always be the same instance as the parameter argument if a previous `CloudStorage` was already added at the same URL and with the same configuration. - */ - public func addStorage(_ storage: T, completion: @escaping (SetupResult) -> Void) { - - let cacheFileURL = storage.cacheFileURL - self.coordinator.performSynchronously { - - if let _ = self.persistentStoreForStorage(storage) { - - DispatchQueue.main.async { - - completion(.success(storage)) - } - return - } - - if let persistentStore = self.coordinator.persistentStore(for: cacheFileURL as URL) { - - if let existingStorage = persistentStore.storageInterface as? T, - storage.matchesPersistentStore(persistentStore) { - - DispatchQueue.main.async { - - completion(.success(existingStorage)) - } - return - } - - let error = CoreStoreError.differentStorageExistsAtURL(existingPersistentStoreURL: cacheFileURL) - Internals.log( - error, - "Failed to add \(Internals.typeName(storage)) at \"\(cacheFileURL)\" because a different \(Internals.typeName(NSPersistentStore.self)) at that URL already exists." - ) - DispatchQueue.main.async { - - completion(.failure(error)) - } - return - } - - do { - - var cloudStorageOptions = storage.cloudStorageOptions - cloudStorageOptions.remove(.recreateLocalStoreOnModelMismatch) - - let storeOptions = storage.dictionary(forOptions: cloudStorageOptions) - do { - - _ = try self.createPersistentStoreFromStorage( - storage, - finalURL: cacheFileURL, - finalStoreOptions: storeOptions - ) - DispatchQueue.main.async { - - completion(.success(storage)) - } - } - catch let error as NSError where storage.cloudStorageOptions.contains(.recreateLocalStoreOnModelMismatch) && error.isCoreDataMigrationError { - - let finalStoreOptions = storage.dictionary(forOptions: storage.cloudStorageOptions) - let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( - ofType: type(of: storage).storeType, - at: cacheFileURL, - options: storeOptions - ) - _ = try self.schemaHistory - .schema(for: metadata) - .flatMap({ try storage.cs_eraseStorageAndWait(soureModel: $0.rawModel()) }) - _ = try self.createPersistentStoreFromStorage( - storage, - finalURL: cacheFileURL, - finalStoreOptions: finalStoreOptions - ) - } - } - catch let error as NSError - where error.code == NSFileReadNoSuchFileError && error.domain == NSCocoaErrorDomain { - - do { - - _ = try self.addStorageAndWait(storage) - - DispatchQueue.main.async { - - completion(.success(storage)) - } - } - catch { - - DispatchQueue.main.async { - - completion(.failure(CoreStoreError(error))) - } - } - } - catch { - - let storeError = CoreStoreError(error) - Internals.log( - storeError, - "Failed to load \(Internals.typeName(NSPersistentStore.self)) metadata." - ) - DispatchQueue.main.async { - - completion(.failure(storeError)) - } - } - } - } - /** Migrates a local storage to match the `DataStack`'s managed object model version. This method does NOT add the migrated store to the data stack. diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift index d2496ad..3010cda 100644 --- a/Sources/DataStack.swift +++ b/Sources/DataStack.swift @@ -370,99 +370,6 @@ public final class DataStack: Equatable { } } - /** - Adds a `CloudStorage` to the stack and blocks until completion. - ``` - guard let storage = ICloudStore( - ubiquitousContentName: "MyAppCloudData", - ubiquitousContentTransactionLogsSubdirectory: "logs/config1", - ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername", - ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0", - configuration: "Config1", - cloudStorageOptions: .recreateLocalStoreOnModelMismatch - ) else { - // iCloud is not available on the device - return - } - try dataStack.addStorageAndWait(storage) - ``` - - parameter storage: the local storage - - throws: a `CoreStoreError` value indicating the failure - - returns: the cloud storage added to the stack. Note that this may not always be the same instance as the parameter argument if a previous `CloudStorage` was already added at the same URL and with the same configuration. - */ - @discardableResult - public func addStorageAndWait(_ storage: T) throws -> T { - - return try self.coordinator.performSynchronously { - - if let _ = self.persistentStoreForStorage(storage) { - - return storage - } - - let cacheFileURL = storage.cacheFileURL - if let persistentStore = self.coordinator.persistentStore(for: cacheFileURL as URL) { - - if let existingStorage = persistentStore.storageInterface as? T, - storage.matchesPersistentStore(persistentStore) { - - return existingStorage - } - - let error = CoreStoreError.differentStorageExistsAtURL(existingPersistentStoreURL: cacheFileURL) - Internals.log( - error, - "Failed to add \(Internals.typeName(storage)) at \"\(cacheFileURL)\" because a different \(Internals.typeName(NSPersistentStore.self)) at that URL already exists." - ) - throw error - } - - do { - - var cloudStorageOptions = storage.cloudStorageOptions - cloudStorageOptions.remove(.recreateLocalStoreOnModelMismatch) - - let storeOptions = storage.dictionary(forOptions: cloudStorageOptions) - do { - - _ = try self.createPersistentStoreFromStorage( - storage, - finalURL: cacheFileURL, - finalStoreOptions: storeOptions - ) - return storage - } - catch let error as NSError where storage.cloudStorageOptions.contains(.recreateLocalStoreOnModelMismatch) && error.isCoreDataMigrationError { - - let finalStoreOptions = storage.dictionary(forOptions: storage.cloudStorageOptions) - let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( - ofType: type(of: storage).storeType, - at: cacheFileURL, - options: storeOptions - ) - _ = try self.schemaHistory - .schema(for: metadata) - .flatMap({ try storage.cs_eraseStorageAndWait(soureModel: $0.rawModel()) }) - _ = try self.createPersistentStoreFromStorage( - storage, - finalURL: cacheFileURL, - finalStoreOptions: finalStoreOptions - ) - return storage - } - } - catch { - - let storeError = CoreStoreError(error) - Internals.log( - storeError, - "Failed to add \(Internals.typeName(storage)) to the stack." - ) - throw storeError - } - } - } - // MARK: 3rd Party Utilities diff --git a/Sources/DiffableDataSource.CollectionView-UIKit.swift b/Sources/DiffableDataSource.CollectionView-UIKit.swift index 2c3e494..985905f 100644 --- a/Sources/DiffableDataSource.CollectionView-UIKit.swift +++ b/Sources/DiffableDataSource.CollectionView-UIKit.swift @@ -59,6 +59,7 @@ extension DiffableDataSource { } ``` `DiffableDataSource.CollectionView` fully handles the reload animations. + - SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources */ open class CollectionView: NSObject, UICollectionViewDataSource { @@ -217,7 +218,8 @@ extension DiffableDataSource { extension UICollectionView { // MARK: FilePrivate - + + // Implementation based on https://github.com/ra1028/DiffableDataSources @nonobjc fileprivate func reload( using stagedChangeset: Internals.DiffableDataUIDispatcher.StagedChangeset, diff --git a/Sources/DiffableDataSource.TableView-UIKit.swift b/Sources/DiffableDataSource.TableView-UIKit.swift index ad91c38..a57cb89 100644 --- a/Sources/DiffableDataSource.TableView-UIKit.swift +++ b/Sources/DiffableDataSource.TableView-UIKit.swift @@ -59,6 +59,7 @@ extension DiffableDataSource { } ``` `DiffableDataSource.TableView` fully handles the reload animations. To turn change the default animation, set the `defaultRowAnimation`. + - SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources */ open class TableView: NSObject, UITableViewDataSource { @@ -261,6 +262,7 @@ extension UITableView { // MARK: FilePrivate + // Implementation based on https://github.com/ra1028/DiffableDataSources @nonobjc fileprivate func reload( using stagedChangeset: Internals.DiffableDataUIDispatcher.StagedChangeset, @@ -282,6 +284,7 @@ extension UITableView { ) } + // Implementation based on https://github.com/ra1028/DiffableDataSources @nonobjc fileprivate func reload( using stagedChangeset: Internals.DiffableDataUIDispatcher.StagedChangeset, diff --git a/Sources/Internals.DiffableDataUIDispatcher.Changeset.swift b/Sources/Internals.DiffableDataUIDispatcher.Changeset.swift index 510905b..bc972be 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.Changeset.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.Changeset.swift @@ -34,6 +34,7 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - ChangeSet + // Implementation based on https://github.com/ra1028/DifferenceKit internal struct Changeset: Equatable where C: Equatable { var data: C diff --git a/Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift b/Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift index 01e8df9..17b7bd7 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift @@ -34,6 +34,7 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - DiffResult + // Implementation based on https://github.com/ra1028/DifferenceKit @usableFromInline internal struct DiffResult { @@ -209,7 +210,8 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - Trace - + + // Implementation based on https://github.com/ra1028/DifferenceKit @usableFromInline internal struct Trace { @@ -228,7 +230,8 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - Occurrence - + + // Implementation based on https://github.com/ra1028/DifferenceKit @usableFromInline internal enum Occurrence { @@ -238,7 +241,8 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - IndicesReference - + + // Implementation based on https://github.com/ra1028/DifferenceKit @usableFromInline internal final class IndicesReference { @@ -277,7 +281,8 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - TableKey - + + // Implementation based on https://github.com/ra1028/DifferenceKit @usableFromInline internal struct TableKey: Hashable { diff --git a/Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift b/Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift index 7ba2359..9d533b3 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift @@ -33,7 +33,8 @@ import Foundation extension Internals.DiffableDataUIDispatcher { // MARK: - StagedChangeset - + + // Implementation based on https://github.com/ra1028/DifferenceKit internal struct StagedChangeset: ExpressibleByArrayLiteral, Equatable, RandomAccessCollection, RangeReplaceableCollection where C: Equatable { @usableFromInline diff --git a/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift b/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift index 3a06159..4348422 100644 --- a/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift +++ b/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift @@ -142,38 +142,13 @@ extension Internals { @objc dynamic func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - switch type { - - case .update, - .move where indexPath == newIndexPath: - let object = anObject as! NSManagedObject - self.reloadedItemIDs.append(object.objectID) - - case .insert, - .delete, - .move: - return - - @unknown default: - return - } + let object = anObject as! NSManagedObject + self.reloadedItemIDs.append(object.objectID) } func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { - switch type { - - case .update: - self.reloadedSectionIDs.append(sectionInfo.name) - - case .insert, - .delete, - .move: - return - - @unknown default: - return - } + self.reloadedSectionIDs.append(sectionInfo.name) } diff --git a/Sources/StorageInterface.swift b/Sources/StorageInterface.swift index 14787c8..3eb8581 100644 --- a/Sources/StorageInterface.swift +++ b/Sources/StorageInterface.swift @@ -161,108 +161,3 @@ extension LocalStorage { && persistentStore.url == self.fileURL } } - - -// MARK: - CloudStorageOptions - -/** - The `CloudStorageOptions` provides settings that tells the `DataStack` how to setup the persistent store for `LocalStorage` implementers. - */ -public struct CloudStorageOptions: OptionSet, ExpressibleByNilLiteral { - - /** - Tells the `DataStack` that the store should not be migrated or recreated, and should simply fail on model mismatch - */ - public static let none = CloudStorageOptions(rawValue: 0) - - /** - Tells the `DataStack` to delete and recreate the local store from the cloud store on model mismatch, otherwise exceptions will be thrown on failure instead - */ - public static let recreateLocalStoreOnModelMismatch = CloudStorageOptions(rawValue: 1 << 0) - - /** - Tells the `DataStack` to allow lightweight migration for the store when added synchronously - */ - public static let allowSynchronousLightweightMigration = CloudStorageOptions(rawValue: 1 << 2) - - - // MARK: OptionSetType - - public init(rawValue: Int) { - - self.rawValue = rawValue - } - - - // MARK: RawRepresentable - - public let rawValue: Int - - - // MARK: ExpressibleByNilLiteral - - public init(nilLiteral: ()) { - - self.rawValue = 0 - } -} - - -// MARK: - CloudStorage - -/** - The `CloudStorage` represents `StorageInterface`s that are synchronized from a cloud-based store. - */ -public protocol CloudStorage: StorageInterface { - - /** - The `NSURL` that points to the store file - */ - var cacheFileURL: URL { get } - - /** - Options that tell the `DataStack` how to setup the persistent store - */ - var cloudStorageOptions: CloudStorageOptions { get } - - /** - The options dictionary for the specified `CloudStorageOptions` - */ - func dictionary(forOptions options: CloudStorageOptions) -> [AnyHashable: Any]? - - /** - Called by the `DataStack` to perform actual deletion of the store file from disk. **Do not call directly!** The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (Cloud stores for example, can set the NSPersistentStoreRemoveUbiquitousMetadataOption option before deleting) - */ - func cs_eraseStorageAndWait(soureModel: NSManagedObjectModel) throws -} - - -// MARK: - Internal - -extension CloudStorage { - - internal func matchesPersistentStore(_ persistentStore: NSPersistentStore) -> Bool { - - guard persistentStore.type == Self.storeType - && persistentStore.configurationName == (self.configuration ?? DataStack.defaultConfigurationName) else { - - return false - } - guard persistentStore.url == self.cacheFileURL else { - - return false - } - guard let persistentStoreOptions = persistentStore.options, - let storeOptions = self.storeOptions else { - - return persistentStore.options == nil && self.storeOptions == nil - } - return storeOptions.reduce(true) { (isMatch, tuple) in - - let (key, value) = tuple - let obj1 = persistentStoreOptions[key] as? NSObject - let obj2 = value as? NSObject - return isMatch && (obj1 == obj2) - } - } -} diff --git a/Sources/diffableDataSource.CollectionView-AppKit.swift b/Sources/diffableDataSource.CollectionView-AppKit.swift index 3c20837..4baafe2 100644 --- a/Sources/diffableDataSource.CollectionView-AppKit.swift +++ b/Sources/diffableDataSource.CollectionView-AppKit.swift @@ -59,6 +59,7 @@ extension DiffableDataSource { } ``` `DiffableDataSource.CollectionView` fully handles the reload animations. + - SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources */ open class CollectionView: NSObject, NSCollectionViewDataSource { @@ -216,7 +217,8 @@ extension DiffableDataSource { extension NSCollectionView { // MARK: FilePrivate - + + // Implementation based on https://github.com/ra1028/DiffableDataSources @nonobjc fileprivate func reload( using stagedChangeset: Internals.DiffableDataUIDispatcher.StagedChangeset,