From 4ddfa95140ef0fc0be2513ea6e7946276fceadf5 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 15 Sep 2021 14:45:13 +0900 Subject: [PATCH] added mechanism to track transaction sources --- .../⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift | 3 + .../⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.swift | 11 + .../⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift | 3 +- .../⭐️Modern.ColorsDemo.UIKit.ListViewController.swift | 19 +- Sources/AsynchronousDataTransaction.swift | 29 +- Sources/BaseDataTransaction.swift | 14 +- Sources/CSDataStack+Transaction.swift | 134 ++++++- Sources/CSSynchronousDataTransaction.swift | 6 +- Sources/CSUnsafeDataTransaction.swift | 88 ++++- Sources/DataStack+Transaction.swift | 40 +- ...aSource.CollectionViewAdapter-AppKit.swift | 27 +- ...taSource.CollectionViewAdapter-UIKit.swift | 31 +- ...bleDataSource.TableViewAdapter-UIKit.swift | 62 ++- Sources/Internals.Closure.swift | 5 +- ...edDiffableDataSourceSnapshotDelegate.swift | 5 +- ...als.FetchedResultsControllerDelegate.swift | 24 +- Sources/ListMonitor.swift | 153 +++++++- Sources/ListObserver.swift | 361 +++++++++++++++++- Sources/ListPublisher.swift | 87 ++++- Sources/NSManagedObjectContext+Setup.swift | 7 +- .../NSManagedObjectContext+Transaction.swift | 76 +++- Sources/ObjectMonitor.swift | 81 +++- Sources/ObjectObserver.swift | 114 +++++- Sources/ObjectPublisher.swift | 59 ++- Sources/SynchronousDataTransaction.swift | 19 +- Sources/UnsafeDataTransaction.swift | 44 ++- 26 files changed, 1323 insertions(+), 179 deletions(-) diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift index 5e78cc8..47a4278 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift @@ -102,6 +102,7 @@ extension Modern.ColorsDemo { try transaction.deleteAll(From()) }, + sourceIdentifier: TransactionSource.clear, completion: { _ in } ) } @@ -113,6 +114,7 @@ extension Modern.ColorsDemo { _ = transaction.create(Into()) }, + sourceIdentifier: TransactionSource.add, completion: { _ in } ) } @@ -127,6 +129,7 @@ extension Modern.ColorsDemo { palette.setRandomHue() } }, + sourceIdentifier: TransactionSource.shuffle, completion: { _ in } ) } diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.swift index 510b292..63aa382 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.swift @@ -69,5 +69,16 @@ extension Modern { ) } } + + + // MARK: - TransactionSource + + enum TransactionSource { + + case add + case delete + case shuffle + case clear + } } } diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift index f342082..b2a5077 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.DetailViewController.swift @@ -91,7 +91,8 @@ extension Modern.ColorsDemo.UIKit { func objectMonitor( _ monitor: ObjectMonitor, didUpdateObject object: Modern.ColorsDemo.Palette, - changedPersistentKeys: Set + changedPersistentKeys: Set, + sourceIdentifier: Any? ) { self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys) diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.ListViewController.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.ListViewController.swift index da31101..7605c61 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.ListViewController.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.UIKit.ListViewController.swift @@ -32,17 +32,25 @@ extension Modern.ColorsDemo.UIKit { ) /** - ⭐️ Sample 2: Once the views are created, we can start binding `ListPublisher` updates to the `DiffableDataSource`. We typically call this at the end of `viewDidLoad`. Note that the `addObserver`'s closure argument will only be called on the succeeding updates, so to immediately display the current values, we need to call `dataSource.apply()` once. + ⭐️ Sample 2: Once the views are created, we can start binding `ListPublisher` updates to the `DiffableDataSource`. We typically call this at the end of `viewDidLoad`. Note that the `addObserver`'s closure argument will only be called on the succeeding updates, so to immediately display the current values, we need to call `dataSource.apply()` once. This example inspects the optional `transactionSource` to determine the source of the update which is helpful for debugging or for fine-tuning animations. */ private func startObservingList() { let dataSource = self.dataSource - self.listPublisher.addObserver(self) { (listPublisher) in + self.listPublisher.addObserver(self, notifyInitial: true) { (listPublisher, transactionSource) in - dataSource.apply(listPublisher.snapshot, animatingDifferences: true) + switch transactionSource as? Modern.ColorsDemo.TransactionSource { + + case .add, + .delete, + .shuffle, + .clear: + dataSource.apply(listPublisher.snapshot, animatingDifferences: true) + + case nil: + dataSource.apply(listPublisher.snapshot, animatingDifferences: false) + } } - - dataSource.apply(self.listPublisher.snapshot, animatingDifferences: false) } /** @@ -74,6 +82,7 @@ extension Modern.ColorsDemo.UIKit { transaction.delete(objectIDs: [itemID]) }, + sourceIdentifier: Modern.ColorsDemo.TransactionSource.delete, completion: { _ in } ) diff --git a/Sources/AsynchronousDataTransaction.swift b/Sources/AsynchronousDataTransaction.swift index 874f098..fd6f304 100644 --- a/Sources/AsynchronousDataTransaction.swift +++ b/Sources/AsynchronousDataTransaction.swift @@ -158,9 +158,19 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { // MARK: Internal - internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue) { + internal init( + mainContext: NSManagedObjectContext, + queue: DispatchQueue, + sourceIdentifier: Any? + ) { - super.init(mainContext: mainContext, queue: queue, supportsUndo: false, bypassesQueueing: false) + super.init( + mainContext: mainContext, + queue: queue, + supportsUndo: false, + bypassesQueueing: false, + sourceIdentifier: sourceIdentifier + ) } internal func autoCommit(_ completion: @escaping (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void) { @@ -168,12 +178,15 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { self.isCommitted = true let group = DispatchGroup() group.enter() - self.context.saveAsynchronouslyWithCompletion { (hasChanges, error) -> Void in - - completion(hasChanges, error) - self.result = (hasChanges, error) - group.leave() - } + self.context.saveAsynchronously( + sourceIdentifier: self.sourceIdentifier, + completion: { (hasChanges, error) -> Void in + + completion(hasChanges, error) + self.result = (hasChanges, error) + group.leave() + } + ) group.wait() self.context.reset() } diff --git a/Sources/BaseDataTransaction.swift b/Sources/BaseDataTransaction.swift index cae9c84..c89999c 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -412,6 +412,11 @@ public /*abstract*/ class BaseDataTransaction { // MARK: 3rd Party Utilities + /** + An arbitrary value that identifies the source of this transaction. Callers of the transaction can provide this value through the `DataStack.perform(...)` methods. + */ + public let sourceIdentifier: Any? + /** Allow external libraries to store custom data in the transaction. App code should rarely have a need for this. ``` @@ -435,7 +440,13 @@ public /*abstract*/ class BaseDataTransaction { internal var isCommitted = false internal var result: (hasChanges: Bool, error: CoreStoreError?)? - internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue, supportsUndo: Bool, bypassesQueueing: Bool) { + internal init( + mainContext: NSManagedObjectContext, + queue: DispatchQueue, + supportsUndo: Bool, + bypassesQueueing: Bool, + sourceIdentifier: Any? + ) { let context = mainContext.temporaryContextInTransactionWithConcurrencyType( queue == .main @@ -446,6 +457,7 @@ public /*abstract*/ class BaseDataTransaction { self.context = context self.supportsUndo = supportsUndo self.bypassesQueueing = bypassesQueueing + self.sourceIdentifier = sourceIdentifier context.parentTransaction = self context.isTransactionContext = true diff --git a/Sources/CSDataStack+Transaction.swift b/Sources/CSDataStack+Transaction.swift index e652e33..f3a74f8 100644 --- a/Sources/CSDataStack+Transaction.swift +++ b/Sources/CSDataStack+Transaction.swift @@ -37,7 +37,9 @@ extension CSDataStack { - parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`. */ @objc - public func beginAsynchronous(_ closure: @escaping (_ transaction: CSAsynchronousDataTransaction) -> Void) { + public func beginAsynchronous( + _ closure: @escaping (_ transaction: CSAsynchronousDataTransaction) -> Void + ) { self.bridgeToSwift.perform( asynchronous: { (transaction) in @@ -57,6 +59,37 @@ extension CSDataStack { ) } + /** + Begins a transaction asynchronously where `NSManagedObject` creates, updates, and deletes can be made. + + - parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. + */ + @objc + public func beginAsynchronous( + _ closure: @escaping (_ transaction: CSAsynchronousDataTransaction) -> Void, + _ sourceIdentifier: Any? + ) { + + self.bridgeToSwift.perform( + asynchronous: { (transaction) in + + let csTransaction = transaction.bridgeToObjectiveC + closure(csTransaction) + if !transaction.isCommitted && transaction.hasChanges { + + Internals.log( + .warning, + message: "The closure for the \(Internals.typeName(csTransaction)) completed without being committed. All changes made within the transaction were discarded." + ) + } + try transaction.cancel() + }, + sourceIdentifier: sourceIdentifier, + completion: { _ in } + ) + } + /** Begins a transaction synchronously where `NSManagedObject` creates, updates, and deletes can be made. @@ -65,7 +98,9 @@ extension CSDataStack { - returns: `YES` if the commit succeeded, `NO` if the commit failed. If `NO`, the `error` argument will hold error information. */ @objc - public func beginSynchronous(_ closure: @escaping (_ transaction: CSSynchronousDataTransaction) -> Void, error: NSErrorPointer) -> Bool { + public func beginSynchronous( + _ closure: @escaping (_ transaction: CSSynchronousDataTransaction) -> Void, + error: NSErrorPointer) -> Bool { return bridge(error) { @@ -94,6 +129,49 @@ extension CSDataStack { } } + /** + Begins a transaction synchronously where `NSManagedObject` creates, updates, and deletes can be made. + + - parameter closure: the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. + - parameter error: the `CSError` pointer that indicates the reason in case of an failure + - returns: `YES` if the commit succeeded, `NO` if the commit failed. If `NO`, the `error` argument will hold error information. + */ + @objc + public func beginSynchronous( + _ closure: @escaping (_ transaction: CSSynchronousDataTransaction) -> Void, + sourceIdentifier: Any?, + error: NSErrorPointer + ) -> Bool { + + return bridge(error) { + + do { + + try self.bridgeToSwift.perform( + synchronous: { (transaction) in + + let csTransaction = transaction.bridgeToObjectiveC + closure(csTransaction) + if !transaction.isCommitted && transaction.hasChanges { + + Internals.log( + .warning, + message: "The closure for the \(Internals.typeName(csTransaction)) completed without being committed. All changes made within the transaction were discarded." + ) + } + try transaction.cancel() + }, + sourceIdentifier: sourceIdentifier + ) + } + catch CoreStoreError.userCancelled { + + return + } + } + } + /** Begins a child transaction where `NSManagedObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. @@ -112,15 +190,61 @@ extension CSDataStack { /** Begins a child transaction where `NSManagedObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. - - prameter supportsUndo: `-undo`, `-redo`, and `-rollback` methods are only available when this parameter is `YES`, otherwise those method will raise an exception. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + To support "undo" methods such as `-undo`, `-redo`, and `-rollback`, use the `-beginSafeWithSupportsUndo:` method passing `YES` to the argument. Without "undo" support, calling those methods will raise an exception. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - returns: a `CSUnsafeDataTransaction` instance where creates, updates, and deletes can be made. */ @objc - public func beginUnsafeWithSupportsUndo(_ supportsUndo: Bool) -> CSUnsafeDataTransaction { + public func beginUnsafeWithSourceIdentifier( + _ sourceIdentifier: Any? + ) -> CSUnsafeDataTransaction { return bridge { - self.bridgeToSwift.beginUnsafe(supportsUndo: supportsUndo) + self.bridgeToSwift.beginUnsafe( + sourceIdentifier: sourceIdentifier + ) + } + } + + /** + Begins a child transaction where `NSManagedObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. + + - parameter supportsUndo: `-undo`, `-redo`, and `-rollback` methods are only available when this parameter is `YES`, otherwise those method will raise an exception. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + - returns: a `CSUnsafeDataTransaction` instance where creates, updates, and deletes can be made. + */ + @objc + public func beginUnsafeWithSupportsUndo( + _ supportsUndo: Bool + ) -> CSUnsafeDataTransaction { + + return bridge { + + self.bridgeToSwift.beginUnsafe( + supportsUndo: supportsUndo + ) + } + } + + /** + Begins a child transaction where `NSManagedObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. + + - parameter supportsUndo: `-undo`, `-redo`, and `-rollback` methods are only available when this parameter is `YES`, otherwise those method will raise an exception. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. + - returns: a `CSUnsafeDataTransaction` instance where creates, updates, and deletes can be made. + */ + @objc + public func beginUnsafeWithSupportsUndo( + _ supportsUndo: Bool, + sourceIdentifier: Any? + ) -> CSUnsafeDataTransaction { + + return bridge { + + self.bridgeToSwift.beginUnsafe( + supportsUndo: supportsUndo, + sourceIdentifier: sourceIdentifier + ) } } diff --git a/Sources/CSSynchronousDataTransaction.swift b/Sources/CSSynchronousDataTransaction.swift index aef28cd..27c2853 100644 --- a/Sources/CSSynchronousDataTransaction.swift +++ b/Sources/CSSynchronousDataTransaction.swift @@ -49,7 +49,11 @@ public final class CSSynchronousDataTransaction: CSBaseDataTransaction, CoreStor return bridge(error) { - if case (_, let error?) = self.bridgeToSwift.context.saveSynchronously(waitForMerge: true) { + let transaction = self.bridgeToSwift + if case (_, let error?) = transaction.context.saveSynchronously( + waitForMerge: true, + sourceIdentifier: transaction.sourceIdentifier + ) { throw error } diff --git a/Sources/CSUnsafeDataTransaction.swift b/Sources/CSUnsafeDataTransaction.swift index 236d7d2..8e331c3 100644 --- a/Sources/CSUnsafeDataTransaction.swift +++ b/Sources/CSUnsafeDataTransaction.swift @@ -46,21 +46,25 @@ public final class CSUnsafeDataTransaction: CSBaseDataTransaction, CoreStoreObje @objc public func commitWithSuccess(_ success: (() -> Void)?, _ failure: ((CSError) -> Void)?) { - self.bridgeToSwift.context.saveAsynchronouslyWithCompletion { (_, error) in - - defer { + let transaction = self.bridgeToSwift + transaction.context.saveAsynchronously( + sourceIdentifier: transaction.sourceIdentifier, + completion: { (_, error) in - withExtendedLifetime(self, {}) + defer { + + withExtendedLifetime(self, {}) + } + if let error = error { + + failure?(error.bridgeToObjectiveC) + } + else { + + success?() + } } - if let error = error { - - failure?(error.bridgeToObjectiveC) - } - else { - - success?() - } - } + ) } /** @@ -74,7 +78,11 @@ public final class CSUnsafeDataTransaction: CSBaseDataTransaction, CoreStoreObje return bridge(error) { - if case (_, let error?) = self.bridgeToSwift.context.saveSynchronously(waitForMerge: true) { + let transaction = self.bridgeToSwift + if case (_, let error?) = transaction.context.saveSynchronously( + waitForMerge: true, + sourceIdentifier: transaction.sourceIdentifier + ) { throw error } @@ -152,15 +160,61 @@ public final class CSUnsafeDataTransaction: CSBaseDataTransaction, CoreStoreObje /** Begins a child transaction where `NSManagedObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. - - prameter supportsUndo: `-undo`, `-redo`, and `-rollback` methods are only available when this parameter is `YES`, otherwise those method will raise an exception. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + To support "undo" methods such as `-undo`, `-redo`, and `-rollback`, use the `-beginSafeWithSupportsUndo:` method passing `YES` to the argument. Without "undo" support, calling those methods will raise an exception. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - returns: a `CSUnsafeDataTransaction` instance where creates, updates, and deletes can be made. */ @objc - public func beginUnsafeWithSupportsUndo(_ supportsUndo: Bool) -> CSUnsafeDataTransaction { + public func beginUnsafeWithSourceIdentifier( + _ sourceIdentifier: Any? + ) -> CSUnsafeDataTransaction { return bridge { - self.bridgeToSwift.beginUnsafe(supportsUndo: supportsUndo) + self.bridgeToSwift.beginUnsafe( + sourceIdentifier: sourceIdentifier + ) + } + } + + /** + Begins a child transaction where `NSManagedObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. + + - parameter supportsUndo: `-undo`, `-redo`, and `-rollback` methods are only available when this parameter is `YES`, otherwise those method will raise an exception. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + - returns: a `CSUnsafeDataTransaction` instance where creates, updates, and deletes can be made. + */ + @objc + public func beginUnsafeWithSupportsUndo( + _ supportsUndo: Bool + ) -> CSUnsafeDataTransaction { + + return bridge { + + self.bridgeToSwift.beginUnsafe( + supportsUndo: supportsUndo + ) + } + } + + /** + Begins a child transaction where `NSManagedObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. + + - parameter supportsUndo: `-undo`, `-redo`, and `-rollback` methods are only available when this parameter is `YES`, otherwise those method will raise an exception. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. + - returns: a `CSUnsafeDataTransaction` instance where creates, updates, and deletes can be made. + */ + @objc + public func beginUnsafeWithSupportsUndo( + _ supportsUndo: Bool, + sourceIdentifier: Any? + ) -> CSUnsafeDataTransaction { + + return bridge { + + self.bridgeToSwift.beginUnsafe( + supportsUndo: supportsUndo, + sourceIdentifier: sourceIdentifier + ) } } diff --git a/Sources/DataStack+Transaction.swift b/Sources/DataStack+Transaction.swift index a7eae2d..90497cf 100644 --- a/Sources/DataStack+Transaction.swift +++ b/Sources/DataStack+Transaction.swift @@ -35,12 +35,18 @@ extension DataStack { Performs a transaction asynchronously where `NSManagedObject` or `CoreStoreObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. On success, the value returned from closure will be the wrapped as `.success(T)` in the `completion`'s `Result`. Any errors thrown from inside the `task` will be reported as `.failure(CoreStoreError)`. To cancel/rollback changes, call `try transaction.cancel()`, which throws a `CoreStoreError.userCancelled`. - parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - parameter completion: the closure executed after the save completes. The `Result` argument of the closure will either wrap the return value of `task`, or any uncaught errors thrown from within `task`. Cancelled `task`s will be indicated by `.failure(error: CoreStoreError.userCancelled)`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`. */ - public func perform(asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, completion: @escaping (AsynchronousDataTransaction.Result) -> Void) { + public func perform( + asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, + sourceIdentifier: Any? = nil, + completion: @escaping (AsynchronousDataTransaction.Result) -> Void + ) { self.perform( asynchronous: task, + sourceIdentifier: sourceIdentifier, success: { completion(.success($0)) }, failure: { completion(.failure($0)) } ) @@ -50,14 +56,21 @@ extension DataStack { Performs a transaction asynchronously where `NSManagedObject` or `CoreStoreObject` creates, updates, and deletes can be made. The changes are commited automatically after the `task` closure returns. On success, the value returned from closure will be the argument of the `success` closure. Any errors thrown from inside the `task` will be wrapped in a `CoreStoreError` and reported in the `failure` closure. To cancel/rollback changes, call `try transaction.cancel()`, which throws a `CoreStoreError.userCancelled`. - parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - parameter success: the closure executed after the save succeeds. The `T` argument of the closure will be the value returned from `task`. - parameter failure: the closure executed if the save fails or if any errors are thrown within `task`. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`. */ - public func perform(asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, success: @escaping (T) -> Void, failure: @escaping (CoreStoreError) -> Void) { + public func perform( + asynchronous task: @escaping (_ transaction: AsynchronousDataTransaction) throws -> T, + sourceIdentifier: Any? = nil, + success: @escaping (T) -> Void, + failure: @escaping (CoreStoreError) -> Void + ) { let transaction = AsynchronousDataTransaction( mainContext: self.rootSavingContext, - queue: self.childTransactionQueue + queue: self.childTransactionQueue, + sourceIdentifier: sourceIdentifier ) transaction.transactionQueue.cs_async { @@ -99,14 +112,20 @@ extension DataStack { - parameter task: the synchronous non-escaping closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`. - parameter waitForAllObservers: When `true`, this method waits for all observers to be notified of the changes before returning. This results in more predictable data update order, but may risk triggering deadlocks. When `false`, this method does not wait for observers to be notified of the changes before returning. This results in lower risk for deadlocks, but the updated data may not have been propagated to the `DataStack` after returning. Defaults to `true`. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - throws: a `CoreStoreError` value indicating the failure. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`. - returns: the value returned from `task` */ - public func perform(synchronous task: ((_ transaction: SynchronousDataTransaction) throws -> T), waitForAllObservers: Bool = true) throws -> T { + public func perform( + synchronous task: ((_ transaction: SynchronousDataTransaction) throws -> T), + waitForAllObservers: Bool = true, + sourceIdentifier: Any? = nil + ) throws -> T { let transaction = SynchronousDataTransaction( mainContext: self.rootSavingContext, - queue: self.childTransactionQueue + queue: self.childTransactionQueue, + sourceIdentifier: sourceIdentifier ) return try transaction.transactionQueue.cs_sync { @@ -141,15 +160,20 @@ extension DataStack { /** Begins a non-contiguous transaction where `NSManagedObject` or `CoreStoreObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. - - prameter supportsUndo: `undo()`, `redo()`, and `rollback()` methods are only available when this parameter is `true`, otherwise those method will raise an exception. Defaults to `false`. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + - parameter supportsUndo: `undo()`, `redo()`, and `rollback()` methods are only available when this parameter is `true`, otherwise those method will raise an exception. Defaults to `false`. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - returns: a `UnsafeDataTransaction` instance where creates, updates, and deletes can be made. */ - public func beginUnsafe(supportsUndo: Bool = false) -> UnsafeDataTransaction { + public func beginUnsafe( + supportsUndo: Bool = false, + sourceIdentifier: Any? = nil + ) -> UnsafeDataTransaction { return UnsafeDataTransaction( mainContext: self.rootSavingContext, queue: DispatchQueue.serial("com.coreStore.dataStack.unsafeTransactionQueue", qos: .userInitiated), - supportsUndo: supportsUndo + supportsUndo: supportsUndo, + sourceIdentifier: sourceIdentifier ) } diff --git a/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift b/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift index 4742cec..39fb38a 100644 --- a/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift +++ b/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift @@ -83,7 +83,12 @@ extension DiffableDataSource { - parameter itemProvider: a closure that configures and returns the `NSCollectionViewItem` for the object */ @nonobjc - public init(collectionView: NSCollectionView, dataStack: DataStack, itemProvider: @escaping (NSCollectionView, IndexPath, O) -> NSCollectionViewItem?, supplementaryViewProvider: @escaping (NSCollectionView, String, IndexPath) -> NSView? = { _, _, _ in nil }) { + public init( + collectionView: NSCollectionView, + dataStack: DataStack, + itemProvider: @escaping (NSCollectionView, IndexPath, O) -> NSCollectionViewItem?, + supplementaryViewProvider: @escaping (NSCollectionView, String, IndexPath) -> NSView? = { _, _, _ in nil } + ) { self.itemProvider = itemProvider self.supplementaryViewProvider = supplementaryViewProvider @@ -97,19 +102,27 @@ extension DiffableDataSource { // MARK: - NSCollectionViewDataSource @objc - public dynamic func numberOfSections(in collectionView: NSCollectionView) -> Int { + public dynamic func numberOfSections( + in collectionView: NSCollectionView + ) -> Int { return self.numberOfSections() } @objc - public dynamic func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + public dynamic func collectionView( + _ collectionView: NSCollectionView, + numberOfItemsInSection section: Int + ) -> Int { return self.numberOfItems(inSection: section) ?? 0 } @objc - open dynamic func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + open dynamic func collectionView( + _ collectionView: NSCollectionView, + itemForRepresentedObjectAt indexPath: IndexPath + ) -> NSCollectionViewItem { guard let objectID = self.itemID(for: indexPath) else { @@ -127,7 +140,11 @@ extension DiffableDataSource { } @objc - open dynamic func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView { + open dynamic func collectionView( + _ collectionView: NSCollectionView, + viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, + at indexPath: IndexPath + ) -> NSView { guard let view = self.supplementaryViewProvider(collectionView, kind, indexPath) else { diff --git a/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift b/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift index 3e2f5f6..4c22f4e 100644 --- a/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift +++ b/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift @@ -83,7 +83,12 @@ extension DiffableDataSource { - parameter cellProvider: a closure that configures and returns the `UICollectionViewCell` for the object - parameter supplementaryViewProvider: an optional closure for providing `UICollectionReusableView` supplementary views. If not set, defaults to returning `nil` */ - public init(collectionView: UICollectionView, dataStack: DataStack, cellProvider: @escaping (UICollectionView, IndexPath, O) -> UICollectionViewCell?, supplementaryViewProvider: @escaping (UICollectionView, String, IndexPath) -> UICollectionReusableView? = { _, _, _ in nil }) { + public init( + collectionView: UICollectionView, + dataStack: DataStack, + cellProvider: @escaping (UICollectionView, IndexPath, O) -> UICollectionViewCell?, + supplementaryViewProvider: @escaping (UICollectionView, String, IndexPath) -> UICollectionReusableView? = { _, _, _ in nil } + ) { self.cellProvider = cellProvider self.supplementaryViewProvider = supplementaryViewProvider @@ -97,19 +102,30 @@ extension DiffableDataSource { // MARK: - UICollectionViewDataSource @objc - public dynamic func numberOfSections(in collectionView: UICollectionView) -> Int { + @MainActor + public dynamic func numberOfSections( + in collectionView: UICollectionView + ) -> Int { return self.numberOfSections() } @objc - public dynamic func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + @MainActor + public dynamic func collectionView( + _ collectionView: UICollectionView, + numberOfItemsInSection section: Int + ) -> Int { return self.numberOfItems(inSection: section) ?? 0 } @objc - open dynamic func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + @MainActor + open dynamic func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { guard let objectID = self.itemID(for: indexPath) else { @@ -127,7 +143,12 @@ extension DiffableDataSource { } @objc - open dynamic func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + @MainActor + open dynamic func collectionView( + _ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath + ) -> UICollectionReusableView { guard let view = self.supplementaryViewProvider(collectionView, kind, indexPath) else { diff --git a/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift b/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift index 117e9d3..3747d96 100644 --- a/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift +++ b/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift @@ -82,7 +82,11 @@ extension DiffableDataSource { - parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from - parameter cellProvider: a closure that configures and returns the `UITableViewCell` for the object */ - public init(tableView: UITableView, dataStack: DataStack, cellProvider: @escaping (UITableView, IndexPath, O) -> UITableViewCell?) { + public init( + tableView: UITableView, + dataStack: DataStack, + cellProvider: @escaping (UITableView, IndexPath, O) -> UITableViewCell? + ) { self.cellProvider = cellProvider super.init(target: .init(tableView), dataStack: dataStack) @@ -102,31 +106,48 @@ extension DiffableDataSource { // MARK: - UITableViewDataSource @objc + @MainActor public dynamic func numberOfSections(in tableView: UITableView) -> Int { return self.numberOfSections() } @objc - public dynamic func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + @MainActor + public dynamic func tableView( + _ tableView: UITableView, + numberOfRowsInSection section: Int + ) -> Int { return self.numberOfItems(inSection: section) ?? 0 } @objc - open dynamic func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + @MainActor + open dynamic func tableView( + _ tableView: UITableView, + titleForHeaderInSection section: Int + ) -> String? { return self.sectionID(for: section) } @objc - open dynamic func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + @MainActor + open dynamic func tableView( + _ tableView: UITableView, + titleForFooterInSection section: Int + ) -> String? { return nil } @objc - open dynamic func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + @MainActor + open dynamic func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { guard let objectID = self.itemID(for: indexPath) else { @@ -144,28 +165,49 @@ extension DiffableDataSource { } @objc - open dynamic func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + @MainActor + open dynamic func tableView( + _ tableView: UITableView, + canEditRowAt indexPath: IndexPath + ) -> Bool { return true } @objc - open dynamic func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + @MainActor + open dynamic func tableView( + _ tableView: UITableView, + editingStyleForRowAt indexPath: IndexPath + ) -> UITableViewCell.EditingStyle { return .delete } @objc - open dynamic func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {} + @MainActor + open dynamic func tableView( + _ tableView: UITableView, + commit editingStyle: UITableViewCell.EditingStyle, + forRowAt indexPath: IndexPath + ) {} @objc - open dynamic func sectionIndexTitles(for tableView: UITableView) -> [String]? { + @MainActor + open dynamic func sectionIndexTitles( + for tableView: UITableView + ) -> [String]? { return nil } @objc - open dynamic func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { + @MainActor + open dynamic func tableView( + _ tableView: UITableView, + sectionForSectionIndexTitle title: String, + at index: Int + ) -> Int { return index } diff --git a/Sources/Internals.Closure.swift b/Sources/Internals.Closure.swift index a7438ec..d904852 100644 --- a/Sources/Internals.Closure.swift +++ b/Sources/Internals.Closure.swift @@ -34,7 +34,10 @@ extension Internals { internal final class Closure { - // MARK: FilePrivate + // MARK: Internal + + internal typealias Arguments = T + internal typealias Result = U internal init(_ closure: @escaping (T) -> U) { diff --git a/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift b/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift index 760be1a..c8d6375 100644 --- a/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift +++ b/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift @@ -43,7 +43,10 @@ internal protocol FetchedDiffableDataSourceSnapshotHandler: AnyObject { var sectionIndexTransformer: (_ sectionName: KeyPathString?) -> String? { get } - func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: Internals.DiffableDataSourceSnapshot) + func controller( + _ controller: NSFetchedResultsController, + didChangeContentWith snapshot: Internals.DiffableDataSourceSnapshot + ) } diff --git a/Sources/Internals.FetchedResultsControllerDelegate.swift b/Sources/Internals.FetchedResultsControllerDelegate.swift index 2931ee0..15da8a7 100644 --- a/Sources/Internals.FetchedResultsControllerDelegate.swift +++ b/Sources/Internals.FetchedResultsControllerDelegate.swift @@ -33,13 +33,28 @@ internal protocol FetchedResultsControllerHandler: AnyObject { var sectionIndexTransformer: (_ sectionName: KeyPathString?) -> String? { get } - func controller(_ controller: NSFetchedResultsController, didChangeObject anObject: Any, atIndexPath indexPath: IndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) + func controller( + _ controller: NSFetchedResultsController, + didChangeObject anObject: Any, + atIndexPath indexPath: IndexPath?, + forChangeType type: NSFetchedResultsChangeType, + newIndexPath: IndexPath? + ) - func controller(_ controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) + func controller( + _ controller: NSFetchedResultsController, + didChangeSection sectionInfo: NSFetchedResultsSectionInfo, + atIndex sectionIndex: Int, + forChangeType type: NSFetchedResultsChangeType + ) - func controllerWillChangeContent(_ controller: NSFetchedResultsController) + func controllerWillChangeContent( + _ controller: NSFetchedResultsController + ) - func controllerDidChangeContent(_ controller: NSFetchedResultsController) + func controllerDidChangeContent( + _ controller: NSFetchedResultsController + ) } @@ -102,7 +117,6 @@ extension Internals { return } - self.handler?.controllerDidChangeContent(controller) } diff --git a/Sources/ListMonitor.swift b/Sources/ListMonitor.swift index 79e0789..ecd2c45 100644 --- a/Sources/ListMonitor.swift +++ b/Sources/ListMonitor.swift @@ -627,7 +627,12 @@ public final class ListMonitor: Hashable { // MARK: Internal - internal convenience init(dataStack: DataStack, from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void) { + internal convenience init( + dataStack: DataStack, + from: From, + sectionBy: SectionBy?, + applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void + ) { self.init( context: dataStack.mainContext, @@ -639,7 +644,13 @@ public final class ListMonitor: Hashable { ) } - internal convenience init(dataStack: DataStack, from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, createAsynchronously: @escaping (ListMonitor) -> Void) { + internal convenience init( + dataStack: DataStack, + from: From, + sectionBy: SectionBy?, + applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, + createAsynchronously: @escaping (ListMonitor) -> Void + ) { self.init( context: dataStack.mainContext, @@ -651,7 +662,12 @@ public final class ListMonitor: Hashable { ) } - internal convenience init(unsafeTransaction: UnsafeDataTransaction, from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void) { + internal convenience init( + unsafeTransaction: UnsafeDataTransaction, + from: From, + sectionBy: SectionBy?, + applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void + ) { self.init( context: unsafeTransaction.context, @@ -663,7 +679,13 @@ public final class ListMonitor: Hashable { ) } - internal convenience init(unsafeTransaction: UnsafeDataTransaction, from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, createAsynchronously: @escaping (ListMonitor) -> Void) { + internal convenience init( + unsafeTransaction: UnsafeDataTransaction, + from: From, + sectionBy: SectionBy?, + applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, + createAsynchronously: @escaping (ListMonitor) -> Void + ) { self.init( context: unsafeTransaction.context, @@ -675,7 +697,12 @@ public final class ListMonitor: Hashable { ) } - internal func registerChangeNotification(_ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, callback: @escaping (_ monitor: ListMonitor) -> Void) { + internal func registerChangeNotification( + _ notificationKey: UnsafeRawPointer, + name: Notification.Name, + toObserver observer: AnyObject, + callback: @escaping (_ monitor: ListMonitor) -> Void + ) { Internals.setAssociatedRetainedObject( Internals.NotificationObserver( @@ -695,7 +722,16 @@ public final class ListMonitor: Hashable { ) } - internal func registerObjectNotification(_ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, callback: @escaping (_ monitor: ListMonitor, _ object: O, _ indexPath: IndexPath?, _ newIndexPath: IndexPath?) -> Void) { + internal func registerObjectNotification( + _ notificationKey: UnsafeRawPointer, + name: Notification.Name, + toObserver observer: AnyObject, + callback: @escaping ( + _ monitor: ListMonitor, + _ object: O, + _ indexPath: IndexPath?, + _ newIndexPath: IndexPath? + ) -> Void) { Internals.setAssociatedRetainedObject( Internals.NotificationObserver( @@ -722,7 +758,16 @@ public final class ListMonitor: Hashable { ) } - internal func registerSectionNotification(_ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, callback: @escaping (_ monitor: ListMonitor, _ sectionInfo: NSFetchedResultsSectionInfo, _ sectionIndex: Int) -> Void) { + internal func registerSectionNotification( + _ notificationKey: UnsafeRawPointer, + name: Notification.Name, + toObserver observer: AnyObject, + callback: @escaping ( + _ monitor: ListMonitor, + _ sectionInfo: NSFetchedResultsSectionInfo, + _ sectionIndex: Int + ) -> Void + ) { Internals.setAssociatedRetainedObject( Internals.NotificationObserver( @@ -745,7 +790,24 @@ public final class ListMonitor: Hashable { ) } - internal func registerObserver(_ observer: U, willChange: @escaping (_ observer: U, _ monitor: ListMonitor) -> Void, didChange: @escaping (_ observer: U, _ monitor: ListMonitor) -> Void, willRefetch: @escaping (_ observer: U, _ monitor: ListMonitor) -> Void, didRefetch: @escaping (_ observer: U, _ monitor: ListMonitor) -> Void) { + internal func registerObserver( + _ observer: U, + willChange: @escaping ( + _ observer: U, + _ monitor: ListMonitor + ) -> Void, + didChange: @escaping ( + _ observer: U, + _ monitor: ListMonitor + ) -> Void, + willRefetch: @escaping ( + _ observer: U, + _ monitor: ListMonitor + ) -> Void, + didRefetch: @escaping ( + _ observer: U, + _ monitor: ListMonitor + ) -> Void) { Internals.assert( Thread.isMainThread, @@ -805,7 +867,33 @@ public final class ListMonitor: Hashable { ) } - internal func registerObserver(_ observer: U, didInsertObject: @escaping (_ observer: U, _ monitor: ListMonitor, _ object: O, _ toIndexPath: IndexPath) -> Void, didDeleteObject: @escaping (_ observer: U, _ monitor: ListMonitor, _ object: O, _ fromIndexPath: IndexPath) -> Void, didUpdateObject: @escaping (_ observer: U, _ monitor: ListMonitor, _ object: O, _ atIndexPath: IndexPath) -> Void, didMoveObject: @escaping (_ observer: U, _ monitor: ListMonitor, _ object: O, _ fromIndexPath: IndexPath, _ toIndexPath: IndexPath) -> Void) { + internal func registerObserver( + _ observer: U, + didInsertObject: @escaping ( + _ observer: U, + _ monitor: ListMonitor, + _ object: O, + _ toIndexPath: IndexPath + ) -> Void, + didDeleteObject: @escaping ( + _ observer: U, + _ monitor: ListMonitor, + _ object: O, + _ fromIndexPath: IndexPath + ) -> Void, + didUpdateObject: @escaping ( + _ observer: U, + _ monitor: ListMonitor, + _ object: O, + _ atIndexPath: IndexPath + ) -> Void, + didMoveObject: @escaping ( + _ observer: U, + _ monitor: ListMonitor, + _ object: O, + _ fromIndexPath: IndexPath, + _ toIndexPath: IndexPath + ) -> Void) { Internals.assert( Thread.isMainThread, @@ -866,7 +954,20 @@ public final class ListMonitor: Hashable { ) } - internal func registerObserver(_ observer: U, didInsertSection: @escaping (_ observer: U, _ monitor: ListMonitor, _ sectionInfo: NSFetchedResultsSectionInfo, _ toIndex: Int) -> Void, didDeleteSection: @escaping (_ observer: U, _ monitor: ListMonitor, _ sectionInfo: NSFetchedResultsSectionInfo, _ fromIndex: Int) -> Void) { + internal func registerObserver( + _ observer: U, + didInsertSection: @escaping ( + _ observer: U, + _ monitor: ListMonitor, + _ sectionInfo: NSFetchedResultsSectionInfo, + _ toIndex: Int + ) -> Void, + didDeleteSection: @escaping ( + _ observer: U, + _ monitor: ListMonitor, + _ sectionInfo: NSFetchedResultsSectionInfo, + _ fromIndex: Int + ) -> Void) { Internals.assert( Thread.isMainThread, @@ -1077,7 +1178,14 @@ public final class ListMonitor: Hashable { private let from: From private let sectionBy: SectionBy? - private init(context: NSManagedObjectContext, transactionQueue: DispatchQueue, from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, createAsynchronously: ((ListMonitor) -> Void)?) { + private init( + context: NSManagedObjectContext, + transactionQueue: DispatchQueue, + from: From, + sectionBy: SectionBy?, + applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, + createAsynchronously: ((ListMonitor) -> Void)? + ) { self.isSectioned = (sectionBy != nil) self.from = from @@ -1294,7 +1402,13 @@ extension ListMonitor: FetchedResultsControllerHandler { return self.sectionByIndexTransformer } - internal func controller(_ controller: NSFetchedResultsController, didChangeObject anObject: Any, atIndexPath indexPath: IndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + internal func controller( + _ controller: NSFetchedResultsController, + didChangeObject anObject: Any, + atIndexPath indexPath: IndexPath?, + forChangeType type: NSFetchedResultsChangeType, + newIndexPath: IndexPath? + ) { switch type { @@ -1344,7 +1458,12 @@ extension ListMonitor: FetchedResultsControllerHandler { } } - internal func controller(_ controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) { + internal func controller( + _ controller: NSFetchedResultsController, + didChangeSection sectionInfo: NSFetchedResultsSectionInfo, + atIndex sectionIndex: Int, + forChangeType type: NSFetchedResultsChangeType + ) { switch type { @@ -1373,7 +1492,9 @@ extension ListMonitor: FetchedResultsControllerHandler { } } - internal func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + internal func controllerWillChangeContent( + _ controller: NSFetchedResultsController + ) { self.taskGroup.enter() NotificationCenter.default.post( @@ -1382,7 +1503,9 @@ extension ListMonitor: FetchedResultsControllerHandler { ) } - internal func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + internal func controllerDidChangeContent( + _ controller: NSFetchedResultsController + ) { defer { diff --git a/Sources/ListObserver.swift b/Sources/ListObserver.swift index ff6f256..30940b1 100644 --- a/Sources/ListObserver.swift +++ b/Sources/ListObserver.swift @@ -51,15 +51,54 @@ public protocol ListObserver: AnyObject { The default implementation does nothing. - parameter monitor: the `ListMonitor` monitoring the list being observed + - parameter sourceIdentifier: an optional identifier provided by the transaction source */ - func listMonitorWillChange(_ monitor: ListMonitor) + func listMonitorWillChange( + _ monitor: ListMonitor, + sourceIdentifier: Any? + ) + + /** + Handles processing just before a change to the observed list occurs. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ListMonitor` monitoring the list being observed + */ + func listMonitorWillChange( + _ monitor: ListMonitor + ) + + /** + Handles processing right after a change to the observed list occurs. (Required) + + - parameter monitor: the `ListMonitor` monitoring the object being observed + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func listMonitorDidChange( + _ monitor: ListMonitor, + sourceIdentifier: Any? + ) /** Handles processing right after a change to the observed list occurs. (Required) - parameter monitor: the `ListMonitor` monitoring the object being observed */ - func listMonitorDidChange(_ monitor: ListMonitor) + func listMonitorDidChange( + _ monitor: ListMonitor + ) + + /** + This method is broadcast from within the `ListMonitor`'s `refetch(...)` method to let observers prepare for the internal `NSFetchedResultsController`'s pending change to its predicate, sort descriptors, etc. (Optional) + + - Important: All `ListMonitor` access between `listMonitorWillRefetch(_:)` and `listMonitorDidRefetch(_:)` will raise and assertion. The actual refetch will happen after the `NSFetchedResultsController`'s last `controllerDidChangeContent(_:)` notification completes. + - parameter monitor: the `ListMonitor` monitoring the object being observed + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func listMonitorWillRefetch( + _ monitor: ListMonitor, + sourceIdentifier: Any? + ) /** This method is broadcast from within the `ListMonitor`'s `refetch(...)` method to let observers prepare for the internal `NSFetchedResultsController`'s pending change to its predicate, sort descriptors, etc. (Optional) @@ -67,7 +106,21 @@ public protocol ListObserver: AnyObject { - Important: All `ListMonitor` access between `listMonitorWillRefetch(_:)` and `listMonitorDidRefetch(_:)` will raise and assertion. The actual refetch will happen after the `NSFetchedResultsController`'s last `controllerDidChangeContent(_:)` notification completes. - parameter monitor: the `ListMonitor` monitoring the object being observed */ - func listMonitorWillRefetch(_ monitor: ListMonitor) + func listMonitorWillRefetch( + _ monitor: ListMonitor + ) + + /** + After the `ListMonitor`'s `refetch(...)` method is called, this method is broadcast after the `NSFetchedResultsController`'s last `controllerDidChangeContent(_:)` notification completes. (Required) + + - Important: When `listMonitorDidRefetch(_:)` is called it should be assumed that all `ListMonitor`'s previous data have been reset, including counts, objects, and persistent stores. + - parameter monitor: the `ListMonitor` monitoring the object being observed + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func listMonitorDidRefetch( + _ monitor: ListMonitor, + sourceIdentifier: Any? + ) /** After the `ListMonitor`'s `refetch(...)` method is called, this method is broadcast after the `NSFetchedResultsController`'s last `controllerDidChangeContent(_:)` notification completes. (Required) @@ -75,7 +128,9 @@ public protocol ListObserver: AnyObject { - Important: When `listMonitorDidRefetch(_:)` is called it should be assumed that all `ListMonitor`'s previous data have been reset, including counts, objects, and persistent stores. - parameter monitor: the `ListMonitor` monitoring the object being observed */ - func listMonitorDidRefetch(_ monitor: ListMonitor) + func listMonitorDidRefetch( + _ monitor: ListMonitor + ) } @@ -83,9 +138,45 @@ public protocol ListObserver: AnyObject { extension ListObserver { - public func listMonitorWillChange(_ monitor: ListMonitor) { } + public func listMonitorWillChange( + _ monitor: ListMonitor, + sourceIdentifier: Any? + ) { + + self.listMonitorWillChange(monitor) + } - public func listMonitorWillRefetch(_ monitor: ListMonitor) { } + public func listMonitorWillChange( + _ monitor: ListMonitor + ) {} + + public func listMonitorDidChange( + _ monitor: ListMonitor, + sourceIdentifier: Any? + ) { + + self.listMonitorDidChange(monitor) + } + + public func listMonitorWillRefetch( + _ monitor: ListMonitor, + sourceIdentifier: Any? + ) { + + self.listMonitorWillRefetch(monitor) + } + + public func listMonitorWillRefetch( + _ monitor: ListMonitor + ) {} + + public func listMonitorDidRefetch( + _ monitor: ListMonitor, + sourceIdentifier: Any? + ) { + + self.listMonitorDidRefetch(monitor) + } } @@ -110,8 +201,44 @@ public protocol ListObjectObserver: ListObserver { - parameter monitor: the `ListMonitor` monitoring the list being observed - parameter object: the entity type for the inserted object - parameter indexPath: the new `NSIndexPath` for the inserted object + - parameter sourceIdentifier: an optional identifier provided by the transaction source */ - func listMonitor(_ monitor: ListMonitor, didInsertObject object: ListEntityType, toIndexPath indexPath: IndexPath) + func listMonitor( + _ monitor: ListMonitor, + didInsertObject object: ListEntityType, + toIndexPath indexPath: IndexPath, + sourceIdentifier: Any? + ) + + /** + Notifies that an object was inserted to the specified `NSIndexPath` in the list. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ListMonitor` monitoring the list being observed + - parameter object: the entity type for the inserted object + - parameter indexPath: the new `NSIndexPath` for the inserted object + */ + func listMonitor( + _ monitor: ListMonitor, + didInsertObject object: ListEntityType, + toIndexPath indexPath: IndexPath + ) + + /** + Notifies that an object was deleted from the specified `NSIndexPath` in the list. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ListMonitor` monitoring the list being observed + - parameter object: the entity type for the deleted object + - parameter indexPath: the `NSIndexPath` for the deleted object + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func listMonitor( + _ monitor: ListMonitor, + didDeleteObject object: ListEntityType, + fromIndexPath indexPath: IndexPath, + sourceIdentifier: Any? + ) /** Notifies that an object was deleted from the specified `NSIndexPath` in the list. (Optional) @@ -121,7 +248,27 @@ public protocol ListObjectObserver: ListObserver { - parameter object: the entity type for the deleted object - parameter indexPath: the `NSIndexPath` for the deleted object */ - func listMonitor(_ monitor: ListMonitor, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) + func listMonitor( + _ monitor: ListMonitor, + didDeleteObject object: ListEntityType, + fromIndexPath indexPath: IndexPath + ) + + /** + Notifies that an object at the specified `NSIndexPath` was updated. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ListMonitor` monitoring the list being observed + - parameter object: the entity type for the updated object + - parameter indexPath: the `NSIndexPath` for the updated object + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func listMonitor( + _ monitor: ListMonitor, + didUpdateObject object: ListEntityType, + atIndexPath indexPath: IndexPath, + sourceIdentifier: Any? + ) /** Notifies that an object at the specified `NSIndexPath` was updated. (Optional) @@ -131,7 +278,29 @@ public protocol ListObjectObserver: ListObserver { - parameter object: the entity type for the updated object - parameter indexPath: the `NSIndexPath` for the updated object */ - func listMonitor(_ monitor: ListMonitor, didUpdateObject object: ListEntityType, atIndexPath indexPath: IndexPath) + func listMonitor( + _ monitor: ListMonitor, + didUpdateObject object: ListEntityType, + atIndexPath indexPath: IndexPath + ) + + /** + Notifies that an object's index changed. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ListMonitor` monitoring the list being observed + - parameter object: the entity type for the moved object + - parameter fromIndexPath: the previous `NSIndexPath` for the moved object + - parameter toIndexPath: the new `NSIndexPath` for the moved object + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func listMonitor( + _ monitor: ListMonitor, + didMoveObject object: ListEntityType, + fromIndexPath: IndexPath, + toIndexPath: IndexPath, + sourceIdentifier: Any? + ) /** Notifies that an object's index changed. (Optional) @@ -142,7 +311,12 @@ public protocol ListObjectObserver: ListObserver { - parameter fromIndexPath: the previous `NSIndexPath` for the moved object - parameter toIndexPath: the new `NSIndexPath` for the moved object */ - func listMonitor(_ monitor: ListMonitor, didMoveObject object: ListEntityType, fromIndexPath: IndexPath, toIndexPath: IndexPath) + func listMonitor( + _ monitor: ListMonitor, + didMoveObject object: ListEntityType, + fromIndexPath: IndexPath, + toIndexPath: IndexPath + ) } @@ -150,13 +324,88 @@ public protocol ListObjectObserver: ListObserver { extension ListObjectObserver { - public func listMonitor(_ monitor: ListMonitor, didInsertObject object: ListEntityType, toIndexPath indexPath: IndexPath) { } + public func listMonitor( + _ monitor: ListMonitor, + didInsertObject object: ListEntityType, + toIndexPath indexPath: IndexPath, + sourceIdentifier: Any? + ) { + + self.listMonitor( + monitor, + didInsertObject: object, + toIndexPath: indexPath + ) + } - public func listMonitor(_ monitor: ListMonitor, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) { } + public func listMonitor( + _ monitor: ListMonitor, + didInsertObject object: ListEntityType, + toIndexPath indexPath: IndexPath + ) {} - public func listMonitor(_ monitor: ListMonitor, didUpdateObject object: ListEntityType, atIndexPath indexPath: IndexPath) { } + public func listMonitor( + _ monitor: ListMonitor, + didDeleteObject object: ListEntityType, + fromIndexPath indexPath: IndexPath, + sourceIdentifier: Any? + ) { + + self.listMonitor( + monitor, + didDeleteObject: object, + fromIndexPath: indexPath + ) + } - public func listMonitor(_ monitor: ListMonitor, didMoveObject object: ListEntityType, fromIndexPath: IndexPath, toIndexPath: IndexPath) { } + public func listMonitor( + _ monitor: ListMonitor, + didDeleteObject object: ListEntityType, + fromIndexPath indexPath: IndexPath + ) {} + + public func listMonitor( + _ monitor: ListMonitor, + didUpdateObject object: ListEntityType, + atIndexPath indexPath: IndexPath, + sourceIdentifier: Any? + ) { + + self.listMonitor( + monitor, + didUpdateObject: object, + atIndexPath: indexPath + ) + } + + public func listMonitor( + _ monitor: ListMonitor, + didUpdateObject object: ListEntityType, + atIndexPath indexPath: IndexPath + ) {} + + public func listMonitor( + _ monitor: ListMonitor, + didMoveObject object: ListEntityType, + fromIndexPath: IndexPath, + toIndexPath: IndexPath, + sourceIdentifier: Any? + ) { + + self.listMonitor( + monitor, + didMoveObject: object, + fromIndexPath: fromIndexPath, + toIndexPath: toIndexPath + ) + } + + public func listMonitor( + _ monitor: ListMonitor, + didMoveObject object: ListEntityType, + fromIndexPath: IndexPath, + toIndexPath: IndexPath + ) {} } @@ -182,8 +431,44 @@ public protocol ListSectionObserver: ListObjectObserver { - parameter monitor: the `ListMonitor` monitoring the list being observed - parameter sectionInfo: the `NSFetchedResultsSectionInfo` for the inserted section - parameter sectionIndex: the new section index for the new section + - parameter sourceIdentifier: an optional identifier provided by the transaction source */ - func listMonitor(_ monitor: ListMonitor, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) + func listMonitor( + _ monitor: ListMonitor, + didInsertSection sectionInfo: NSFetchedResultsSectionInfo, + toSectionIndex sectionIndex: Int, + sourceIdentifier: Any? + ) + + /** + Notifies that a section was inserted at the specified index. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ListMonitor` monitoring the list being observed + - parameter sectionInfo: the `NSFetchedResultsSectionInfo` for the inserted section + - parameter sectionIndex: the new section index for the new section + */ + func listMonitor( + _ monitor: ListMonitor, + didInsertSection sectionInfo: NSFetchedResultsSectionInfo, + toSectionIndex sectionIndex: Int + ) + + /** + Notifies that a section was inserted at the specified index. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ListMonitor` monitoring the list being observed + - parameter sectionInfo: the `NSFetchedResultsSectionInfo` for the deleted section + - parameter sectionIndex: the previous section index for the deleted section + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func listMonitor( + _ monitor: ListMonitor, + didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, + fromSectionIndex sectionIndex: Int, + sourceIdentifier: Any? + ) /** Notifies that a section was inserted at the specified index. (Optional) @@ -193,7 +478,11 @@ public protocol ListSectionObserver: ListObjectObserver { - parameter sectionInfo: the `NSFetchedResultsSectionInfo` for the deleted section - parameter sectionIndex: the previous section index for the deleted section */ - func listMonitor(_ monitor: ListMonitor, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) + func listMonitor( + _ monitor: ListMonitor, + didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, + fromSectionIndex sectionIndex: Int + ) } @@ -201,7 +490,43 @@ public protocol ListSectionObserver: ListObjectObserver { extension ListSectionObserver { - public func listMonitor(_ monitor: ListMonitor, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) { } + public func listMonitor( + _ monitor: ListMonitor, + didInsertSection sectionInfo: NSFetchedResultsSectionInfo, + toSectionIndex sectionIndex: Int, + sourceIdentifier: Any? + ) { + + self.listMonitor( + monitor, + didInsertSection: sectionInfo, + toSectionIndex: sectionIndex + ) + } - public func listMonitor(_ monitor: ListMonitor, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) { } + public func listMonitor( + _ monitor: ListMonitor, + didInsertSection sectionInfo: NSFetchedResultsSectionInfo, + toSectionIndex sectionIndex: Int + ) {} + + public func listMonitor( + _ monitor: ListMonitor, + didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, + fromSectionIndex sectionIndex: Int, + sourceIdentifier: Any? + ) { + + self.listMonitor( + monitor, + didDeleteSection: sectionInfo, + fromSectionIndex: sectionIndex + ) + } + + public func listMonitor( + _ monitor: ListMonitor, + didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, + fromSectionIndex sectionIndex: Int + ) {} } diff --git a/Sources/ListPublisher.swift b/Sources/ListPublisher.swift index 9a35e4f..8574567 100644 --- a/Sources/ListPublisher.swift +++ b/Sources/ListPublisher.swift @@ -76,13 +76,7 @@ public final class ListPublisher: Hashable { /** A snapshot of the latest state of this list */ - public fileprivate(set) var snapshot: ListSnapshot = .init() { - - didSet { - - self.notifyObservers() - } - } + public private(set) var snapshot: ListSnapshot = .init() // MARK: Public (Observers) @@ -111,7 +105,7 @@ public final class ListPublisher: Hashable { "Attempted to add an observer of type \(Internals.typeName(observer)) outside the main thread." ) self.observers.setObject( - Internals.Closure(callback), + Internals.Closure({ callback($0.listPublisher) }), forKey: observer ) if notifyInitial { @@ -119,6 +113,44 @@ public final class ListPublisher: Hashable { callback(self) } } + + /** + Registers an object as an observer to be notified when changes to the `ListPublisher`'s snapshot occur. + + To prevent retain-cycles, `ListPublisher` only keeps `weak` references to its observers. + + For thread safety, this method needs to be called from the main thread. An assertion failure will occur (on debug builds only) if called from any thread other than the main thread. + + Calling `addObserver(_:_:)` multiple times on the same observer is safe. + + - parameter observer: an object to become owner of the specified `callback` + - parameter notifyInitial: if `true`, the callback is executed immediately with the current publisher state. Otherwise only succeeding updates will notify the observer. Default value is `false`. + - parameter initialSourceIdentifier: an optional value that identifies the initial callback invocation if `notifyInitial` is `true`. + - parameter callback: the closure to execute when changes occur + */ + public func addObserver( + _ observer: T, + notifyInitial: Bool = false, + initialSourceIdentifier: Any? = nil, + _ callback: @escaping ( + _ listPublisher: ListPublisher, + _ sourceIdentifier: Any? + ) -> Void + ) { + + Internals.assert( + Thread.isMainThread, + "Attempted to add an observer of type \(Internals.typeName(observer)) outside the main thread." + ) + self.observers.setObject( + Internals.Closure(callback), + forKey: observer + ) + if notifyInitial { + + callback(self, initialSourceIdentifier) + } + } /** Unregisters an object from receiving notifications for changes to the `ListPublisher`'s snapshot. @@ -301,6 +333,20 @@ public final class ListPublisher: Hashable { } + // MARK: FilePrivate + + fileprivate typealias ObserverClosureType = Internals.Closure<(listPublisher: ListPublisher, sourceIdentifier: Any?), Void> + + fileprivate func set( + snapshot: ListSnapshot, + sourceIdentifier: Any? + ) { + + self.snapshot = snapshot + self.notifyObservers(sourceIdentifier: sourceIdentifier) + } + + // MARK: Private private var query: ( @@ -315,7 +361,7 @@ public final class ListPublisher: Hashable { private var observerForWillChangePersistentStore: Internals.NotificationObserver! private var observerForDidChangePersistentStore: Internals.NotificationObserver! - private lazy var observers: NSMapTable, Void>> = .weakToStrongObjects() + private lazy var observers: NSMapTable = .weakToStrongObjects() private static func recreateFetchedResultsController(context: NSManagedObjectContext, from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void) -> (controller: Internals.CoreStoreFetchedResultsController, delegate: Internals.FetchedDiffableDataSourceSnapshotDelegate) { @@ -359,15 +405,19 @@ public final class ListPublisher: Hashable { try! self.fetchedResultsController.performFetchFromSpecifiedStores() } - private func notifyObservers() { + private func notifyObservers(sourceIdentifier: Any?) { guard let enumerator = self.observers.objectEnumerator() else { return } + let arguments: ObserverClosureType.Arguments = ( + listPublisher: self, + sourceIdentifier: sourceIdentifier + ) for closure in enumerator { - (closure as! Internals.Closure, Void>).invoke(with: self) + (closure as! ObserverClosureType).invoke(with: arguments) } } } @@ -384,11 +434,18 @@ extension ListPublisher: FetchedDiffableDataSourceSnapshotHandler { return self.query.sectionIndexTransformer } - internal func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: Internals.DiffableDataSourceSnapshot) { + internal func controller( + _ controller: NSFetchedResultsController, + didChangeContentWith snapshot: Internals.DiffableDataSourceSnapshot + ) { - self.snapshot = .init( - diffableSnapshot: snapshot, - context: controller.managedObjectContext + let context = controller.managedObjectContext + self.set( + snapshot: .init( + diffableSnapshot: snapshot, + context: context + ), + sourceIdentifier: context.saveMetadata?.sourceIdentifier ) } } diff --git a/Sources/NSManagedObjectContext+Setup.swift b/Sources/NSManagedObjectContext+Setup.swift index cb82bce..f179adf 100644 --- a/Sources/NSManagedObjectContext+Setup.swift +++ b/Sources/NSManagedObjectContext+Setup.swift @@ -113,6 +113,10 @@ extension NSManagedObjectContext { return } + + let saveMetadata = rootContext.saveMetadata + context.saveMetadata = saveMetadata + let mergeChanges = { () -> Void in if let updatedObjects = (note.userInfo?[NSUpdatedObjectsKey] as? Set) { @@ -123,8 +127,9 @@ extension NSManagedObjectContext { } } context.mergeChanges(fromContextDidSave: note) + context.saveMetadata = nil } - if rootContext.isSavingSynchronously == true { + if case true? = saveMetadata?.isSavingSynchronously { context.performAndWait(mergeChanges) } diff --git a/Sources/NSManagedObjectContext+Transaction.swift b/Sources/NSManagedObjectContext+Transaction.swift index 4bbcdb5..9162070 100644 --- a/Sources/NSManagedObjectContext+Transaction.swift +++ b/Sources/NSManagedObjectContext+Transaction.swift @@ -54,21 +54,21 @@ extension NSManagedObjectContext { } @nonobjc - internal var isSavingSynchronously: Bool? { + internal var saveMetadata: SaveMetadata? { get { - let value: NSNumber? = Internals.getAssociatedObjectForKey( - &PropertyKeys.isSavingSynchronously, + let value: SaveMetadata? = Internals.getAssociatedObjectForKey( + &PropertyKeys.saveMetadata, inObject: self ) - return value?.boolValue + return value } set { - Internals.setAssociatedWeakObject( - newValue.flatMap { NSNumber(value: $0) }, - forKey: &PropertyKeys.isSavingSynchronously, + Internals.setAssociatedRetainedObject( + newValue, + forKey: &PropertyKeys.saveMetadata, inObject: self ) } @@ -140,7 +140,10 @@ extension NSManagedObjectContext { } @nonobjc - internal func saveSynchronously(waitForMerge: Bool) -> (hasChanges: Bool, error: CoreStoreError?) { + internal func saveSynchronously( + waitForMerge: Bool, + sourceIdentifier: Any? + ) -> (hasChanges: Bool, error: CoreStoreError?) { var result: (hasChanges: Bool, error: CoreStoreError?) = (false, nil) self.performAndWait { @@ -151,9 +154,12 @@ extension NSManagedObjectContext { } do { - self.isSavingSynchronously = waitForMerge + self.saveMetadata = .init( + isSavingSynchronously: waitForMerge, + sourceIdentifier: sourceIdentifier + ) try self.save() - self.isSavingSynchronously = nil + self.saveMetadata = nil } catch { @@ -167,7 +173,10 @@ extension NSManagedObjectContext { } if let parentContext = self.parent, self.shouldCascadeSavesToParent { - let (_, error) = parentContext.saveSynchronously(waitForMerge: waitForMerge) + let (_, error) = parentContext.saveSynchronously( + waitForMerge: waitForMerge, + sourceIdentifier: sourceIdentifier + ) result = (true, error) } else { @@ -179,7 +188,10 @@ extension NSManagedObjectContext { } @nonobjc - internal func saveAsynchronouslyWithCompletion(_ completion: @escaping (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in }) { + internal func saveAsynchronously( + sourceIdentifier: Any?, + completion: @escaping (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in } + ) { self.perform { @@ -193,9 +205,12 @@ extension NSManagedObjectContext { } do { - self.isSavingSynchronously = false + self.saveMetadata = .init( + isSavingSynchronously: false, + sourceIdentifier: sourceIdentifier + ) try self.save() - self.isSavingSynchronously = nil + self.saveMetadata = nil } catch { @@ -212,10 +227,13 @@ extension NSManagedObjectContext { } if self.shouldCascadeSavesToParent, let parentContext = self.parent { - parentContext.saveAsynchronouslyWithCompletion { (_, error) in - - completion(true, error) - } + parentContext.saveAsynchronously( + sourceIdentifier: sourceIdentifier, + completion: { (_, error) in + + completion(true, error) + } + ) } else { @@ -234,12 +252,32 @@ extension NSManagedObjectContext { } + // MARK: - SaveMetadata + + internal final class SaveMetadata { + + // MARK: Internal + + internal let isSavingSynchronously: Bool + internal let sourceIdentifier: Any? + + internal init( + isSavingSynchronously: Bool, + sourceIdentifier: Any? + ) { + + self.isSavingSynchronously = isSavingSynchronously + self.sourceIdentifier = sourceIdentifier + } + } + + // MARK: Private private struct PropertyKeys { static var parentTransaction: Void? - static var isSavingSynchronously: Void? + static var saveMetadata: Void? static var isTransactionContext: Void? static var isDataStackContext: Void? } diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift index dcc9c1b..f81b983 100644 --- a/Sources/ObjectMonitor.swift +++ b/Sources/ObjectMonitor.swift @@ -78,15 +78,28 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati observer, willChangeObject: { (observer, monitor, object) in - observer.objectMonitor(monitor, willUpdateObject: object) + observer.objectMonitor( + monitor, + willUpdateObject: object, + sourceIdentifier: monitor.context.saveMetadata?.sourceIdentifier + ) }, didDeleteObject: { (observer, monitor, object) in - observer.objectMonitor(monitor, didDeleteObject: object) + observer.objectMonitor( + monitor, + didDeleteObject: object, + sourceIdentifier: monitor.context.saveMetadata?.sourceIdentifier + ) }, didUpdateObject: { (observer, monitor, object, changedPersistentKeys) in - observer.objectMonitor(monitor, didUpdateObject: object, changedPersistentKeys: changedPersistentKeys) + observer.objectMonitor( + monitor, + didUpdateObject: object, + changedPersistentKeys: changedPersistentKeys, + sourceIdentifier: monitor.context.saveMetadata?.sourceIdentifier + ) } ) } @@ -197,7 +210,10 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati // MARK: Internal - internal init(objectID: O.ObjectID, context: NSManagedObjectContext) { + internal init( + objectID: O.ObjectID, + context: NSManagedObjectContext + ) { let fetchRequest = Internals.CoreStoreFetchRequest() fetchRequest.entity = objectID.entity @@ -227,7 +243,25 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati self.lastCommittedAttributes = (self.object?.cs_toRaw().committedValues(forKeys: nil) as? [String: NSObject]) ?? [:] } - internal func registerObserver(_ observer: U, willChangeObject: @escaping (_ observer: U, _ monitor: ObjectMonitor, _ object: O) -> Void, didDeleteObject: @escaping (_ observer: U, _ monitor: ObjectMonitor, _ object: O) -> Void, didUpdateObject: @escaping (_ observer: U, _ monitor: ObjectMonitor, _ object: O, _ changedPersistentKeys: Set) -> Void) { + internal func registerObserver( + _ observer: U, + willChangeObject: @escaping ( + _ observer: U, + _ monitor: ObjectMonitor, + _ object: O + ) -> Void, + didDeleteObject: @escaping ( + _ observer: U, + _ monitor: ObjectMonitor, + _ object: O + ) -> Void, + didUpdateObject: @escaping ( + _ observer: U, + _ monitor: ObjectMonitor, + _ object: O, + _ changedPersistentKeys: Set + ) -> Void + ) { Internals.assert( Thread.isMainThread, @@ -323,7 +357,12 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati return self.fetchedResultsController.managedObjectContext } - private func registerChangeNotification(_ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, callback: @escaping (_ monitor: ObjectMonitor) -> Void) { + private func registerChangeNotification( + _ notificationKey: UnsafeRawPointer, + name: Notification.Name, + toObserver observer: AnyObject, + callback: @escaping (_ monitor: ObjectMonitor) -> Void + ) { Internals.setAssociatedRetainedObject( Internals.NotificationObserver( @@ -343,7 +382,12 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati ) } - private func registerObjectNotification(_ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, callback: @escaping (_ monitor: ObjectMonitor, _ object: O) -> Void) { + private func registerObjectNotification( + _ notificationKey: UnsafeRawPointer, + name: Notification.Name, + toObserver observer: AnyObject, + callback: @escaping (_ monitor: ObjectMonitor, _ object: O) -> Void + ) { Internals.setAssociatedRetainedObject( Internals.NotificationObserver( @@ -384,7 +428,9 @@ extension ObjectMonitor: FetchedResultsControllerHandler { return { _ in nil } } - internal func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + internal func controllerWillChangeContent( + _ controller: NSFetchedResultsController + ) { NotificationCenter.default.post( name: Notification.Name.objectMonitorWillChangeObject, @@ -392,9 +438,17 @@ extension ObjectMonitor: FetchedResultsControllerHandler { ) } - internal func controllerDidChangeContent(_ controller: NSFetchedResultsController) { } + internal func controllerDidChangeContent( + _ controller: NSFetchedResultsController + ) {} - internal func controller(_ controller: NSFetchedResultsController, didChangeObject anObject: Any, atIndexPath indexPath: IndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + internal func controller( + _ controller: NSFetchedResultsController, + didChangeObject anObject: Any, + atIndexPath indexPath: IndexPath?, + forChangeType type: NSFetchedResultsChangeType, + newIndexPath: IndexPath? + ) { switch type { @@ -418,7 +472,12 @@ extension ObjectMonitor: FetchedResultsControllerHandler { } } - internal func controller(_ controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) { } + internal func controller( + _ controller: NSFetchedResultsController, + didChangeSection sectionInfo: NSFetchedResultsSectionInfo, + atIndex sectionIndex: Int, + forChangeType type: NSFetchedResultsChangeType + ) {} } diff --git a/Sources/ObjectObserver.swift b/Sources/ObjectObserver.swift index f2fa9cb..109ed92 100644 --- a/Sources/ObjectObserver.swift +++ b/Sources/ObjectObserver.swift @@ -49,8 +49,41 @@ public protocol ObjectObserver: AnyObject { - parameter monitor: the `ObjectMonitor` monitoring the object being observed - parameter object: the `DynamicObject` instance being observed + - parameter sourceIdentifier: an optional identifier provided by the transaction source */ - func objectMonitor(_ monitor: ObjectMonitor, willUpdateObject object: ObjectEntityType) + func objectMonitor( + _ monitor: ObjectMonitor, + willUpdateObject object: ObjectEntityType, + sourceIdentifier: Any? + ) + + /** + Handles processing just before a change to the observed `object` occurs. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ObjectMonitor` monitoring the object being observed + - parameter object: the `DynamicObject` instance being observed + */ + func objectMonitor( + _ monitor: ObjectMonitor, + willUpdateObject object: ObjectEntityType + ) + + /** + Handles processing right after a change to the observed `object` occurs. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ObjectMonitor` monitoring the object being observed + - parameter object: the `DynamicObject` instance being observed + - parameter changedPersistentKeys: a `Set` of key paths for the attributes that were changed. Note that `changedPersistentKeys` only contains keys for attributes/relationships present in the persistent store, thus transient properties will not be reported. + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func objectMonitor( + _ monitor: ObjectMonitor, + didUpdateObject object: ObjectEntityType, + changedPersistentKeys: Set, + sourceIdentifier: Any? + ) /** Handles processing right after a change to the observed `object` occurs. (Optional) @@ -60,7 +93,25 @@ public protocol ObjectObserver: AnyObject { - parameter object: the `DynamicObject` instance being observed - parameter changedPersistentKeys: a `Set` of key paths for the attributes that were changed. Note that `changedPersistentKeys` only contains keys for attributes/relationships present in the persistent store, thus transient properties will not be reported. */ - func objectMonitor(_ monitor: ObjectMonitor, didUpdateObject object: ObjectEntityType, changedPersistentKeys: Set) + func objectMonitor( + _ monitor: ObjectMonitor, + didUpdateObject object: ObjectEntityType, + changedPersistentKeys: Set + ) + + /** + Handles processing right after `object` is deleted. (Optional) + The default implementation does nothing. + + - parameter monitor: the `ObjectMonitor` monitoring the object being observed + - parameter object: the `DynamicObject` instance being observed + - parameter sourceIdentifier: an optional identifier provided by the transaction source + */ + func objectMonitor( + _ monitor: ObjectMonitor, + didDeleteObject object: ObjectEntityType, + sourceIdentifier: Any? + ) /** Handles processing right after `object` is deleted. (Optional) @@ -69,7 +120,10 @@ public protocol ObjectObserver: AnyObject { - parameter monitor: the `ObjectMonitor` monitoring the object being observed - parameter object: the `DynamicObject` instance being observed */ - func objectMonitor(_ monitor: ObjectMonitor, didDeleteObject object: ObjectEntityType) + func objectMonitor( + _ monitor: ObjectMonitor, + didDeleteObject object: ObjectEntityType + ) } @@ -77,9 +131,57 @@ public protocol ObjectObserver: AnyObject { extension ObjectObserver { - public func objectMonitor(_ monitor: ObjectMonitor, willUpdateObject object: ObjectEntityType) { } + public func objectMonitor( + _ monitor: ObjectMonitor, + willUpdateObject object: ObjectEntityType, + sourceIdentifier: Any? + ) { + + self.objectMonitor( + monitor, + willUpdateObject: object + ) + } - public func objectMonitor(_ monitor: ObjectMonitor, didUpdateObject object: ObjectEntityType, changedPersistentKeys: Set) { } + public func objectMonitor( + _ monitor: ObjectMonitor, + willUpdateObject object: ObjectEntityType + ) {} - public func objectMonitor(_ monitor: ObjectMonitor, didDeleteObject object: ObjectEntityType) { } + public func objectMonitor( + _ monitor: ObjectMonitor, + didUpdateObject object: ObjectEntityType, + changedPersistentKeys: Set, + sourceIdentifier: Any? + ) { + + self.objectMonitor( + monitor, + didUpdateObject: object, + changedPersistentKeys: changedPersistentKeys + ) + } + + public func objectMonitor( + _ monitor: ObjectMonitor, + didUpdateObject object: ObjectEntityType, + changedPersistentKeys: Set + ) {} + + public func objectMonitor( + _ monitor: ObjectMonitor, + didDeleteObject object: ObjectEntityType, + sourceIdentifier: Any? + ) { + + self.objectMonitor( + monitor, + didDeleteObject: object + ) + } + + public func objectMonitor( + _ monitor: ObjectMonitor, + didDeleteObject object: ObjectEntityType + ) {} } diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index c958bee..2e0ebac 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -87,7 +87,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash "Attempted to add an observer of type \(Internals.typeName(observer)) outside the main thread." ) self.observers.setObject( - Internals.Closure(callback), + Internals.Closure({ callback($0.objectPublisher) }), forKey: observer ) _ = self.lazySnapshot @@ -97,6 +97,46 @@ public final class ObjectPublisher: ObjectRepresentation, Hash callback(self) } } + + /** + Registers an object as an observer to be notified when changes to the `ObjectPublisher`'s snapshot occur. + + To prevent retain-cycles, `ObjectPublisher` only keeps `weak` references to its observers. + + For thread safety, this method needs to be called from the main thread. An assertion failure will occur (on debug builds only) if called from any thread other than the main thread. + + Calling `addObserver(_:_:)` multiple times on the same observer is safe. + + - parameter observer: an object to become owner of the specified `callback` + - parameter notifyInitial: if `true`, the callback is executed immediately with the current publisher state. Otherwise only succeeding updates will notify the observer. Default value is `false`. + - parameter initialSourceIdentifier: an optional value that identifies the initial callback invocation if `notifyInitial` is `true`. + - parameter callback: the closure to execute when changes occur + */ + public func addObserver( + _ observer: T, + notifyInitial: Bool = false, + initialSourceIdentifier: Any? = nil, + _ callback: @escaping ( + _ objectPublisher: ObjectPublisher, + _ sourceIdentifier: Any? + ) -> Void + ) { + + Internals.assert( + Thread.isMainThread, + "Attempted to add an observer of type \(Internals.typeName(observer)) outside the main thread." + ) + self.observers.setObject( + Internals.Closure(callback), + forKey: observer + ) + _ = self.lazySnapshot + + if notifyInitial { + + callback(self, initialSourceIdentifier) + } + } /** Unregisters an object from receiving notifications for changes to the `ObjectPublisher`'s snapshot. @@ -215,6 +255,8 @@ public final class ObjectPublisher: ObjectRepresentation, Hash // MARK: FilePrivate + + fileprivate typealias ObserverClosureType = Internals.Closure<(objectPublisher: ObjectPublisher, sourceIdentifier: Any?), Void> fileprivate init(objectID: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot?) { @@ -237,12 +279,12 @@ public final class ObjectPublisher: ObjectRepresentation, Hash self.object = nil self.$lazySnapshot.reset({ nil }) - self.notifyObservers() + self.notifyObservers(sourceIdentifier: self.context.saveMetadata) } else if updatedIDs.contains(objectID) { self.$lazySnapshot.reset({ initializer(objectID, context) }) - self.notifyObservers() + self.notifyObservers(sourceIdentifier: self.context.saveMetadata) } } return initializer(objectID, context) @@ -258,18 +300,21 @@ public final class ObjectPublisher: ObjectRepresentation, Hash @Internals.LazyNonmutating(uninitialized: ()) private var lazySnapshot: ObjectSnapshot? - private lazy var observers: NSMapTable, Void>> = .weakToStrongObjects() + private lazy var observers: NSMapTable = .weakToStrongObjects() - private func notifyObservers() { + private func notifyObservers(sourceIdentifier: Any?) { guard let enumerator = self.observers.objectEnumerator() else { return } + let arguments: ObserverClosureType.Arguments = ( + objectPublisher: self, + sourceIdentifier: sourceIdentifier + ) for closure in enumerator { - (closure as! Internals.Closure, Void>).invoke(with: self) + (closure as! ObserverClosureType).invoke(with: arguments) } } } diff --git a/Sources/SynchronousDataTransaction.swift b/Sources/SynchronousDataTransaction.swift index 1ad24f8..d31459d 100644 --- a/Sources/SynchronousDataTransaction.swift +++ b/Sources/SynchronousDataTransaction.swift @@ -147,15 +147,28 @@ public final class SynchronousDataTransaction: BaseDataTransaction { // MARK: Internal - internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue) { + internal init( + mainContext: NSManagedObjectContext, + queue: DispatchQueue, + sourceIdentifier: Any? + ) { - super.init(mainContext: mainContext, queue: queue, supportsUndo: false, bypassesQueueing: false) + super.init( + mainContext: mainContext, + queue: queue, + supportsUndo: false, + bypassesQueueing: false, + sourceIdentifier: sourceIdentifier + ) } internal func autoCommit(waitForMerge: Bool) -> (hasChanges: Bool, error: CoreStoreError?) { self.isCommitted = true - let result = self.context.saveSynchronously(waitForMerge: waitForMerge) + let result = self.context.saveSynchronously( + waitForMerge: waitForMerge, + sourceIdentifier: self.sourceIdentifier + ) self.result = result defer { diff --git a/Sources/UnsafeDataTransaction.swift b/Sources/UnsafeDataTransaction.swift index 103bce2..2ec3f25 100644 --- a/Sources/UnsafeDataTransaction.swift +++ b/Sources/UnsafeDataTransaction.swift @@ -43,11 +43,14 @@ public final class UnsafeDataTransaction: BaseDataTransaction { */ public func commit(_ completion: @escaping (_ error: CoreStoreError?) -> Void) { - self.context.saveAsynchronouslyWithCompletion { (_, error) in - - completion(error) - withExtendedLifetime(self, {}) - } + self.context.saveAsynchronously( + sourceIdentifier: self.sourceIdentifier, + completion: { (_, error) in + + completion(error) + withExtendedLifetime(self, {}) + } + ) } /** @@ -57,7 +60,10 @@ public final class UnsafeDataTransaction: BaseDataTransaction { */ public func commitAndWait() throws { - if case (_, let error?) = self.context.saveSynchronously(waitForMerge: true) { + if case (_, let error?) = self.context.saveSynchronously( + waitForMerge: true, + sourceIdentifier: self.sourceIdentifier + ) { throw error } @@ -126,23 +132,39 @@ public final class UnsafeDataTransaction: BaseDataTransaction { /** Begins a child transaction where `NSManagedObject` or `CoreStoreObject` creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. - - prameter supportsUndo: `undo()`, `redo()`, and `rollback()` methods are only available when this parameter is `true`, otherwise those method will raise an exception. Defaults to `false`. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + - parameter supportsUndo: `undo()`, `redo()`, and `rollback()` methods are only available when this parameter is `true`, otherwise those method will raise an exception. Defaults to `false`. Note that turning on Undo support may heavily impact performance especially on iOS or watchOS where memory is limited. + - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - returns: an `UnsafeDataTransaction` instance where creates, updates, and deletes can be made. */ - public func beginUnsafe(supportsUndo: Bool = false) -> UnsafeDataTransaction { + public func beginUnsafe( + supportsUndo: Bool = false, + sourceIdentifier: Any? = nil + ) -> UnsafeDataTransaction { return UnsafeDataTransaction( mainContext: self.context, queue: self.transactionQueue, - supportsUndo: supportsUndo + supportsUndo: supportsUndo, + sourceIdentifier: sourceIdentifier ) } // MARK: Internal - internal init(mainContext: NSManagedObjectContext, queue: DispatchQueue, supportsUndo: Bool) { + internal init( + mainContext: NSManagedObjectContext, + queue: DispatchQueue, + supportsUndo: Bool, + sourceIdentifier: Any? + ) { - super.init(mainContext: mainContext, queue: queue, supportsUndo: supportsUndo, bypassesQueueing: true) + super.init( + mainContext: mainContext, + queue: queue, + supportsUndo: supportsUndo, + bypassesQueueing: true, + sourceIdentifier: sourceIdentifier + ) } }