diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 4f18530..d0f474e 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -427,6 +427,14 @@ B549F6751E56A92800FBAB2D /* CoreDataNativeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B549F6721E56A92800FBAB2D /* CoreDataNativeType.swift */; }; B549F6761E56A92800FBAB2D /* CoreDataNativeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B549F6721E56A92800FBAB2D /* CoreDataNativeType.swift */; }; B54A6A551BA15F2A007870FD /* Internals.FetchedResultsControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54A6A541BA15F2A007870FD /* Internals.FetchedResultsControllerDelegate.swift */; }; + B54CAFCD264381AE0055485D /* Internals.PersistentHistoryObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54CAFCC264381AE0055485D /* Internals.PersistentHistoryObserver.swift */; }; + B54CAFCE264381AE0055485D /* Internals.PersistentHistoryObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54CAFCC264381AE0055485D /* Internals.PersistentHistoryObserver.swift */; }; + B54CAFCF264381AE0055485D /* Internals.PersistentHistoryObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54CAFCC264381AE0055485D /* Internals.PersistentHistoryObserver.swift */; }; + B54CAFD0264381AE0055485D /* Internals.PersistentHistoryObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54CAFCC264381AE0055485D /* Internals.PersistentHistoryObserver.swift */; }; + B54CAFD22643FE460055485D /* Internals.PersistentHistoryFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54CAFD12643FE460055485D /* Internals.PersistentHistoryFetcher.swift */; }; + B54CAFD32643FE460055485D /* Internals.PersistentHistoryFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54CAFD12643FE460055485D /* Internals.PersistentHistoryFetcher.swift */; }; + B54CAFD42643FE470055485D /* Internals.PersistentHistoryFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54CAFD12643FE460055485D /* Internals.PersistentHistoryFetcher.swift */; }; + B54CAFD52643FE470055485D /* Internals.PersistentHistoryFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54CAFD12643FE460055485D /* Internals.PersistentHistoryFetcher.swift */; }; B5519A401CA1B17B002BEF78 /* ErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5519A3F1CA1B17B002BEF78 /* ErrorTests.swift */; }; B5519A411CA1B17B002BEF78 /* ErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5519A3F1CA1B17B002BEF78 /* ErrorTests.swift */; }; B5519A421CA1B17B002BEF78 /* ErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5519A3F1CA1B17B002BEF78 /* ErrorTests.swift */; }; @@ -1092,6 +1100,8 @@ B549F65D1E569C7400FBAB2D /* QueryableAttributeType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryableAttributeType.swift; sourceTree = ""; }; B549F6721E56A92800FBAB2D /* CoreDataNativeType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataNativeType.swift; sourceTree = ""; }; B54A6A541BA15F2A007870FD /* Internals.FetchedResultsControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Internals.FetchedResultsControllerDelegate.swift; sourceTree = ""; }; + B54CAFCC264381AE0055485D /* Internals.PersistentHistoryObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.PersistentHistoryObserver.swift; sourceTree = ""; }; + B54CAFD12643FE460055485D /* Internals.PersistentHistoryFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.PersistentHistoryFetcher.swift; sourceTree = ""; }; B5519A3F1CA1B17B002BEF78 /* ErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorTests.swift; sourceTree = ""; }; B5519A491CA1F4FB002BEF78 /* CSError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSError.swift; sourceTree = ""; }; B5519A581CA2008C002BEF78 /* CSBaseDataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSBaseDataTransaction.swift; sourceTree = ""; }; @@ -1992,6 +2002,8 @@ B5BF7FCA234D80910070E741 /* Internals.LazyNonmutating.swift */, B5FAD6AB1B51285300714891 /* Internals.MigrationManager.swift */, B5E84F2B1AFF849C0064E85B /* Internals.NotificationObserver.swift */, + B54CAFD12643FE460055485D /* Internals.PersistentHistoryFetcher.swift */, + B54CAFCC264381AE0055485D /* Internals.PersistentHistoryObserver.swift */, B5277676234F265F0056BE9F /* Internals.SharedNotificationObserver.swift */, B50E17602351FA66004F033C /* Internals.Closure.swift */, B5E84F2D1AFF849C0064E85B /* Internals.WeakObject.swift */, @@ -2487,10 +2499,12 @@ B5E84EE81AFF84610064E85B /* CoreStoreLogger.swift in Sources */, B50C3EF923D1987D00B29880 /* FieldCoders.Json.swift in Sources */, B56923C91EB82410007C4DC9 /* NSManagedObjectModel+Migration.swift in Sources */, + B54CAFD22643FE460055485D /* Internals.PersistentHistoryFetcher.swift in Sources */, B50C3EDA23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */, B56923E41EB827F5007C4DC9 /* CustomSchemaMappingProvider.swift in Sources */, B58D0C631EAA0C7E003EDD87 /* NSManagedObject+DynamicModel.swift in Sources */, B533C4DB1D7D4BFA001383CB /* DispatchQueue+CoreStore.swift in Sources */, + B54CAFCD264381AE0055485D /* Internals.PersistentHistoryObserver.swift in Sources */, B559CD491CAA8C6D00E4D58B /* CSStorageInterface.swift in Sources */, B5E84F311AFF849C0064E85B /* Internals.WeakObject.swift in Sources */, B52FEC742596DBE100368BFB /* ObjectReader.swift in Sources */, @@ -2741,10 +2755,12 @@ B50C3EDB23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */, 82BA18CB1C4BBD6400A0916E /* NSManagedObject+Convenience.swift in Sources */, 82BA18B51C4BBD3F00A0916E /* BaseDataTransaction+Querying.swift in Sources */, + B54CAFD32643FE460055485D /* Internals.PersistentHistoryFetcher.swift in Sources */, B501FDDF1CA8D05000BE22EF /* CSSectionBy.swift in Sources */, B5BF7FAE234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */, B538BA781D15B3E30003A766 /* CoreStoreBridge.m in Sources */, B52FEC752596DBE100368BFB /* ObjectReader.swift in Sources */, + B54CAFCE264381AE0055485D /* Internals.PersistentHistoryObserver.swift in Sources */, B51260801E97A18000402229 /* CoreStoreObject+Convenience.swift in Sources */, 82BA18D31C4BBD7100A0916E /* NSManagedObjectContext+CoreStore.swift in Sources */, 82BA18AD1C4BBD3100A0916E /* UnsafeDataTransaction.swift in Sources */, @@ -2995,10 +3011,12 @@ B5220E141D130614009BC71E /* DataStack+Observing.swift in Sources */, B50C3EFC23D1987D00B29880 /* FieldCoders.Json.swift in Sources */, B50C3EDD23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */, + B54CAFD52643FE470055485D /* Internals.PersistentHistoryFetcher.swift in Sources */, B5DE522E230BD7D600A22534 /* Internals.swift in Sources */, B5E2222E1CA51B6E00BA2E95 /* CSUnsafeDataTransaction.swift in Sources */, B5220E191D130761009BC71E /* ListMonitor.swift in Sources */, B5220E181D130711009BC71E /* ObjectObserver.swift in Sources */, + B54CAFD0264381AE0055485D /* Internals.PersistentHistoryObserver.swift in Sources */, B5220E251D13088E009BC71E /* ListObserver.swift in Sources */, B538BA7A1D15B3E30003A766 /* CoreStoreBridge.m in Sources */, B52FEC772596DBE100368BFB /* ObjectReader.swift in Sources */, @@ -3249,10 +3267,12 @@ B50C3EFB23D1987D00B29880 /* FieldCoders.Json.swift in Sources */, B50C3EDC23D0545800B29880 /* FieldAttributeProtocol.swift in Sources */, B5DE522D230BD7D600A22534 /* Internals.swift in Sources */, + B54CAFD42643FE470055485D /* Internals.PersistentHistoryFetcher.swift in Sources */, B56321851BD65216006C9394 /* CoreStore+Logging.swift in Sources */, B56321921BD65216006C9394 /* BaseDataTransaction+Querying.swift in Sources */, B501FDE01CA8D05000BE22EF /* CSSectionBy.swift in Sources */, B5BF7FAF234C41E90070E741 /* Internals.DiffableDataSourceSnapshot.swift in Sources */, + B54CAFCF264381AE0055485D /* Internals.PersistentHistoryObserver.swift in Sources */, B538BA791D15B3E30003A766 /* CoreStoreBridge.m in Sources */, B52FEC762596DBE100368BFB /* ObjectReader.swift in Sources */, B51260811E97A18000402229 /* CoreStoreObject+Convenience.swift in Sources */, diff --git a/Sources/Internals.AppGroupsManager.swift b/Sources/Internals.AppGroupsManager.swift index 3a30eef..96be1c7 100644 --- a/Sources/Internals.AppGroupsManager.swift +++ b/Sources/Internals.AppGroupsManager.swift @@ -35,17 +35,19 @@ extension Internals { internal enum AppGroupsManager { // MARK: Internal - + + internal typealias AppGroupID = String + internal typealias BundleID = String - internal typealias StoreID = UUID + internal typealias StorageID = UUID @discardableResult internal static func register( - appGroupIdentifier: String, + appGroupIdentifier: AppGroupID, subdirectory: String?, fileName: String - ) throws -> StoreID { + ) throws -> StorageID { let bundleID = self.bundleID() let indexMetadataURL = self.indexMetadataURL( @@ -56,7 +58,7 @@ extension Internals { initializer: IndexMetadata.init, { metadata in - return metadata.fetchOrCreateStoreID( + return metadata.fetchOrCreateStorageID( bundleID: bundleID, subdirectory: subdirectory, fileName: fileName @@ -64,10 +66,30 @@ extension Internals { } ) } + + internal static func existingToken( + appGroupIdentifier: AppGroupID, + bundleID: BundleID, + storageID: StorageID + ) throws -> NSPersistentHistoryToken? { + + let storageMetadataURL = self.storageMetadataURL( + appGroupIdentifier: appGroupIdentifier, + bundleID: bundleID, + storageID: storageID + ) + return try self.metadata( + forReadingAt: storageMetadataURL, + { (metadata: StorageMetadata) in + + return metadata.persistentHistoryToken + } + ) + } internal static func existingToken( - appGroupIdentifier: String, - subdirectory: String, + appGroupIdentifier: AppGroupID, + subdirectory: String?, fileName: String ) throws -> NSPersistentHistoryToken? { @@ -76,11 +98,11 @@ extension Internals { appGroupIdentifier: appGroupIdentifier ) guard - let storeID = try self.metadata( + let storageID = try self.metadata( forReadingAt: indexMetadataURL, { (metadata: IndexMetadata) in - return metadata.fetchStoreID( + return metadata.fetchStorageID( bundleID: bundleID, subdirectory: subdirectory, fileName: fileName @@ -91,24 +113,39 @@ extension Internals { return nil } + return try self.existingToken( + appGroupIdentifier: appGroupIdentifier, + bundleID: bundleID, + storageID: storageID + ) + } + + internal static func setExistingToken( + _ newToken: NSPersistentHistoryToken, + appGroupIdentifier: AppGroupID, + bundleID: BundleID, + storageID: StorageID + ) throws { + let storageMetadataURL = self.storageMetadataURL( appGroupIdentifier: appGroupIdentifier, bundleID: bundleID, - storeID: storeID + storageID: storageID ) - return try self.metadata( - forReadingAt: storageMetadataURL, - { (metadata: StorageMetadata) in - - return metadata.persistentHistoryToken + try self.metadata( + forWritingAt: storageMetadataURL, + initializer: StorageMetadata.init, + { metadata in + + metadata.persistentHistoryToken = newToken } ) } internal static func setExistingToken( _ newToken: NSPersistentHistoryToken, - appGroupIdentifier: String, - subdirectory: String, + appGroupIdentifier: AppGroupID, + subdirectory: String?, fileName: String ) throws { @@ -117,11 +154,11 @@ extension Internals { appGroupIdentifier: appGroupIdentifier ) guard - let storeID = try self.metadata( + let storageID = try self.metadata( forReadingAt: indexMetadataURL, { (metadata: IndexMetadata) in - return metadata.fetchStoreID( + return metadata.fetchStorageID( bundleID: bundleID, subdirectory: subdirectory, fileName: fileName @@ -132,18 +169,11 @@ extension Internals { return } - let storageMetadataURL = self.storageMetadataURL( + try self.setExistingToken( + newToken, appGroupIdentifier: appGroupIdentifier, bundleID: bundleID, - storeID: storeID - ) - try self.metadata( - forWritingAt: storageMetadataURL, - initializer: StorageMetadata.init, - { metadata in - - metadata.persistentHistoryToken = newToken - } + storageID: storageID ) } @@ -151,7 +181,7 @@ extension Internals { // MARK: Private private static func appGroupContainerURL( - appGroupIdentifier: String + appGroupIdentifier: AppGroupID ) -> URL { guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else { @@ -163,7 +193,7 @@ extension Internals { } private static func indexMetadataURL( - appGroupIdentifier: String + appGroupIdentifier: AppGroupID ) -> URL { return self.appGroupContainerURL(appGroupIdentifier: appGroupIdentifier) @@ -171,15 +201,15 @@ extension Internals { } private static func storageMetadataURL( - appGroupIdentifier: String, + appGroupIdentifier: AppGroupID, bundleID: BundleID, - storeID: StoreID + storageID: StorageID ) -> URL { return self .appGroupContainerURL(appGroupIdentifier: appGroupIdentifier) .appendingPathComponent(bundleID, isDirectory: true) - .appendingPathComponent(storeID.uuidString, isDirectory: false) + .appendingPathComponent(storageID.uuidString, isDirectory: false) .appendingPathExtension("meta") } @@ -314,23 +344,29 @@ extension Internals { // MARK: FilePrivate - fileprivate func fetchStoreID( + fileprivate func fetchStorageID( bundleID: BundleID, subdirectory: String?, fileName: String - ) -> StoreID? { + ) -> StorageID? { - let fileTag = Self.createFileTag(subdirectory: subdirectory, fileName: fileName) + let fileTag = Self.createFileTag( + subdirectory: subdirectory, + fileName: fileName + ) return self.contents[bundleID, default: [:]][fileTag] } - fileprivate mutating func fetchOrCreateStoreID( + fileprivate mutating func fetchOrCreateStorageID( bundleID: BundleID, subdirectory: String?, fileName: String - ) -> StoreID { - - let fileTag = Self.createFileTag(subdirectory: subdirectory, fileName: fileName) + ) -> StorageID { + + let fileTag = Self.createFileTag( + subdirectory: subdirectory, + fileName: fileName + ) return self.contents[bundleID, default: [:]][fileTag, default: UUID()] } diff --git a/Sources/Internals.PersistentHistoryFetcher.swift b/Sources/Internals.PersistentHistoryFetcher.swift new file mode 100644 index 0000000..80f6ba2 --- /dev/null +++ b/Sources/Internals.PersistentHistoryFetcher.swift @@ -0,0 +1,102 @@ +// +// Internals.PersistentHistoryFetcher.swift +// CoreStore +// +// Copyright © 2020 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import CoreData +import Foundation + + +// MARK: - Internals + +extension Internals { + + // MARK: - PersistentHistoryFetcher + + internal struct PersistentHistoryFetcher { + + // MARK: Internal + + internal let context: NSManagedObjectContext + internal let token: NSPersistentHistoryToken? + + internal init( + context: NSManagedObjectContext, + token: NSPersistentHistoryToken? + ) { + + self.context = context + self.token = token + } + + internal func fetch() throws -> [NSPersistentHistoryTransaction] { + + let request = self.newFetchRequest() + let fetchResult = try self.context.execute(request) as! NSPersistentHistoryResult + return fetchResult.result as! [NSPersistentHistoryTransaction] + } + + + // MARK: Private + + private func newFetchRequest() -> NSPersistentHistoryChangeRequest { + + let historyFetchRequest = NSPersistentHistoryChangeRequest.fetchHistory( + after: self.token + ) + guard #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) else { + + return historyFetchRequest + } + guard let transactionFetchRequest = NSPersistentHistoryTransaction.fetchRequest else { + + return historyFetchRequest + } + + let context = self.context + transactionFetchRequest.predicate = NSCompoundPredicate( + type: .and, + subpredicates: [ + context.transactionAuthor.map { author in + NSPredicate( + format: "%K != %@", + #keyPath(NSPersistentHistoryTransaction.author), + author + ) + }, + context.name.map { contextName in + NSPredicate( + format: "%K != %@", + #keyPath(NSPersistentHistoryTransaction.contextName), + contextName + ) + } + ] + .compactMap({ $0 }) + ) + historyFetchRequest.fetchRequest = transactionFetchRequest + + return historyFetchRequest + } + } +} diff --git a/Sources/Internals.PersistentHistoryObserver.swift b/Sources/Internals.PersistentHistoryObserver.swift new file mode 100644 index 0000000..3cac4d9 --- /dev/null +++ b/Sources/Internals.PersistentHistoryObserver.swift @@ -0,0 +1,149 @@ +// +// Internals.PersistentHistoryObserver.swift +// CoreStore +// +// Copyright © 2020 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import CoreData +import Foundation + + +// MARK: - Internals + +extension Internals { + + // MARK: - PersistentHistoryObserver + + internal final class PersistentHistoryObserver { + + // MARK: Internal + + internal init( + appGroupIdentifier: AppGroupsManager.AppGroupID, + bundleID: AppGroupsManager.BundleID, + storageID: AppGroupsManager.StorageID, + dataStack: DataStack + ) { + + self.appGroupIdentifier = appGroupIdentifier + self.bundleID = bundleID + self.storageID = storageID + self.dataStack = dataStack + } + + internal func startObserving() { + + if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { + + self.observerForRemoteChangeNotification = Internals.NotificationObserver( + notificationName: .NSPersistentStoreRemoteChange, + object: self, + closure: { [weak self] (note) -> Void in + + guard let `self` = self else { + + return + } + self.historyQueue.addOperation { [weak self] in + + self?.processPersistentHistory() + } + } + ) + } + else { + + #warning("TODO: handle remote changes for iOS 11 & 12") + } + } + + + // MARK: Private + + private let appGroupIdentifier: AppGroupsManager.AppGroupID + private let bundleID: AppGroupsManager.BundleID + private let storageID: AppGroupsManager.StorageID + private let dataStack: DataStack + + private var observerForRemoteChangeNotification: Internals.NotificationObserver? + + private lazy var historyQueue: OperationQueue = Internals.with { + + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + } + + private func processPersistentHistory() { + + self.dataStack.perform( + asynchronous: { transaction -> NSPersistentHistoryToken? in + + let token = try AppGroupsManager.existingToken( + appGroupIdentifier: self.appGroupIdentifier, + bundleID: self.bundleID, + storageID: self.storageID + ) + + let context = transaction.unsafeContext() + let fetcher = PersistentHistoryFetcher( + context: context, + token: token + ) + let history = try fetcher.fetch() + guard !history.isEmpty else { + + return nil + } + + context.merge(fromPersistentHistory: history) + + return history.last?.token + }, + success: { token in + + guard let token = token else { + + return + } + do { + + try AppGroupsManager.setExistingToken( + token, + appGroupIdentifier: self.appGroupIdentifier, + bundleID: self.bundleID, + storageID: self.storageID + ) + } + catch { + + #warning("TODO: handle error") + } + }, + failure: { error in + + #warning("TODO: handle error") + } + ) + } + } +} diff --git a/Sources/NSManagedObjectContext+Transaction.swift b/Sources/NSManagedObjectContext+Transaction.swift index 3992d44..3748c7a 100644 --- a/Sources/NSManagedObjectContext+Transaction.swift +++ b/Sources/NSManagedObjectContext+Transaction.swift @@ -232,6 +232,22 @@ extension NSManagedObjectContext { self.refreshAllObjects() } + + @nonobjc + internal func merge(fromPersistentHistory transactions: [NSPersistentHistoryTransaction]) { + + for transaction in transactions { + + guard let userInfo = transaction.objectIDNotification().userInfo else { + + continue + } + NSManagedObjectContext.mergeChanges( + fromRemoteContextSave: userInfo, + into: [self] + ) + } + } // MARK: Private