如何正确使用查询生成令牌?
How to properly use query generation tokens?
我正在尝试使用 CoreData 和 QueryGenerationTokens 运行示例项目。该项目的本质是在计时器上提交对后台上下文的更改(模拟来自服务器的更改),直到对 UI 采取操作(例如,按下按钮).
目前,我在后台上下文中保存了更改(每 5 秒添加并保存一个实体)并且它们自动进入视图上下文(正如预期的那样,.automaticallyMergesChangesFromParent 设置为 true)。 出现问题的地方,我在当前查询生成令牌发生任何这些更改之前固定视图上下文。我希望视图不会随着添加的背景项目而更新,但会随着它们而更新。所以查询生成令牌似乎没有效果?
我想到的一些可能的问题:
- 我从 Apple 找到的唯一 example 没有显示他们将它与获取结果控制器一起使用(我在 SwiftUI 中使用
@FetchRequest
,我'几乎完全可以肯定本质上是一样的),所以这可能会产生影响?
- .automaticallyMergeChangesFromParent 不应使用,我应该尝试合并策略,但这似乎也不起作用,从概念上讲,查询生成令牌似乎应该与此一起使用并固定到生成,无论合并。
视图代码 - 处理从视图上下文加载数据
// Environment object before fetch request necessary
// Passed in wherever main view is instantiated through .environment()
@Environment(\.managedObjectContext) var managedObjectContext
// Acts as fetched results controller, loading data automatically into items upon the managedObjectContext updating
// ExampleCoreDataEntity.retrieveItemsFetchRequest() is an extension method on the entity to easily get a fetch request for the type with sorting
@FetchRequest(fetchRequest: ExampleCoreDataEntity.retrieveItemsFetchRequest()) var items: FetchedResults<ExampleCoreDataEntity>
var body: some View {
NavigationView {
// Button to refresh and bring in changes
Button(
action: {
do {
try self.managedObjectContext.setQueryGenerationFrom(.current)
self.managedObjectContext.refreshAllObjects()
} catch {
print(error.localizedDescription)
}
},
label: { Image(systemName: "arrow.clockwise") }
)
// Creates a table of items sorted by the entity itself (entities conform to Hashable)
List(self.items, id: \.self) { item in
Text(item.name ?? "")
}
}
}
SceneDelegate 中的代码(SwiftUI 应用程序启动的地方)我还初始化了 CoreData 所需的内容:
// Setup and pass in environment of managed object context to main view
// via extension on persistent container that sets up CoreData stack
let managedObjectContext = NSPersistentContainer.shared.viewContext
do {
try managedObjectContext.setQueryGenerationFrom(.current)
} catch {
print(error.localizedDescription)
}
let view = MainView().environment(\.managedObjectContext, managedObjectContext)
// Setup background adding
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(backgroundCode), userInfo: nil, repeats: true)
// Setup window and pass in main view
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: view)
后台添加数据功能:
@objc func backgroundCode() {
ExampleCoreDataEntity.create(names: ["background object"], in: backgroundContext, shouldSave: true)
}
NSPersistentContainer 的设置:
extension NSPersistentContainer {
private struct SharedContainerStorage {
static let container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Core_Data_Exploration")
container.loadPersistentStores { (description, error) in
guard error == nil else {
assertionFailure("CoreData: Unresolved error \(error!.localizedDescription)")
return
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
return container
}()
}
static var shared: NSPersistentContainer {
return SharedContainerStorage.container
}
}
Create/Read/Update/Delete 在实体上运行:
extension ExampleCoreDataEntity {
static func retrieveItemsFetchRequest() -> NSFetchRequest<ExampleCoreDataEntity> {
let request: NSFetchRequest<ExampleCoreDataEntity> = ExampleCoreDataEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \ExampleCoreDataEntity.creationDate, ascending: false)]
return request
}
static func create(names: [String], in context: NSManagedObjectContext, shouldSave save: Bool = false) {
context.perform {
names.forEach { name in
let item = ExampleCoreDataEntity(context: context)
item.name = name
item.creationDate = Date()
item.identifier = UUID()
}
do {
if save {
try context.save()
}
} catch {
// print error
}
}
}
func delete(in context: NSManagedObjectContext, shouldSave save: Bool = false) {
context.perform {
let name = self.name ?? "an item"
context.delete(context.object(with: self.objectID))
do {
if save {
try context.save()
}
} catch {
// print error
}
}
}
}
问题是 container.viewContext.automaticallyMergesChangesFromParent = true
在使用查询生成标记时,属性 不能设置为 true。我回到这个问题并在 automaticallyMergesChangesFromParent
:
上面记录的 NSManagedObjectContext
的 header 中找到了这个
Setting this property to YES when the context is pinned to a non-current query generation is not supported.
让它工作的一般流程如下:
- 将查询生成令牌设置为
.current
- 在视图上下文中调用
.refreshAllObjects()
- 在获取的结果控制器上调用
.performFetch()
这最后一部分违背了我在原始问题中使用的代码 @FetchRequest
- 目前,我想不出一种方法来手动重新获取它似乎不是非常 hacky。为了解决这个问题,我创建了一个中间存储 class,其中包含一个采用其委托协议的 FetchedResultsController
。该商店还采用了 ObservableObject
,它允许 SwiftUI 视图在采用商店的 ObservableObject
中调用 objectWillChange.send()
时监听其更改。
在问题中链接到的文档中,您会看到它说:
"在任何固定的上下文上调用 save()、reset()、mergeChangesFromContextDidSaveNotification: 或 mergeChangesFromRemoteContextSave(:intoContexts:) 将自动将其推进到操作的最新版本,然后重置其查询生成到 currentQueryGenerationToken。"
您看到后台保存的变化的原因是 automaticallyMergesChangesFromParent
只是为了方便 mergeChangesFromContextDidSaveNotification
所以你们这一代人在进步。
仅供参考,这是另一个使用查询生成的示例项目 - Synchronizing a Local Store to the Cloud
这里是相关代码:
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
A class to set up the Core Data stack, observe Core Data notifications, process persistent history, and deduplicate tags.
*/
import Foundation
import CoreData
// MARK: - Core Data Stack
/**
Core Data stack setup including history processing.
*/
class CoreDataStack {
/**
A persistent container that can load cloud-backed and non-cloud stores.
*/
lazy var persistentContainer: NSPersistentContainer = {
// Create a container that can load CloudKit-backed stores
let container = NSPersistentCloudKitContainer(name: "CoreDataCloudKitDemo")
// Enable history tracking and remote notifications
guard let description = container.persistentStoreDescriptions.first else {
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (_, error) in
guard let error = error as NSError? else { return }
fatalError("###\(#function): Failed to load persistent stores:\(error)")
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
// Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container)
return container
}()
/**
Track the last history token processed for a store, and write its value to file.
The historyQueue reads the token when executing operations, and updates it after processing is complete.
*/
private var lastHistoryToken: NSPersistentHistoryToken? = nil {
didSet {
guard let token = lastHistoryToken,
let data = try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true) else { return }
do {
try data.write(to: tokenFile)
} catch {
print("###\(#function): Failed to write token data. Error = \(error)")
}
}
}
/**
The file URL for persisting the persistent history token.
*/
private lazy var tokenFile: URL = {
let url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create persistent container URL. Error = \(error)")
}
}
return url.appendingPathComponent("token.data", isDirectory: false)
}()
/**
An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
*/
private lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
/**
The URL of the thumbnail folder.
*/
static var attachmentFolder: URL = {
var url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
url = url.appendingPathComponent("attachments", isDirectory: true)
// Create it if it doesn’t exist.
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create thumbnail folder URL: \(error)")
}
}
return url
}()
init() {
// Load the last token from the token file.
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
print("###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
}
}
}
}
// MARK: - Notifications
extension CoreDataStack {
/**
Handle remote store change notifications (.NSPersistentStoreRemoteChange).
*/
@objc
func storeRemoteChange(_ notification: Notification) {
print("###\(#function): Merging changes from the other persistent store coordinator.")
// Process persistent history to merge changes from other coordinators.
historyQueue.addOperation {
self.processPersistentHistory()
}
}
}
/**
Custom notifications in this sample.
*/
extension Notification.Name {
static let didFindRelevantTransactions = Notification.Name("didFindRelevantTransactions")
}
// MARK: - Persistent history processing
extension CoreDataStack {
/**
Process persistent history, posting any relevant transactions to the current view.
*/
func processPersistentHistory() {
let taskContext = persistentContainer.newBackgroundContext()
taskContext.performAndWait {
// Fetch history received from outside the app since the last token
let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
historyFetchRequest.predicate = NSPredicate(format: "author != %@", appTransactionAuthorName)
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
request.fetchRequest = historyFetchRequest
let result = (try? taskContext.execute(request)) as? NSPersistentHistoryResult
guard let transactions = result?.result as? [NSPersistentHistoryTransaction],
!transactions.isEmpty
else { return }
// Post transactions relevant to the current view.
DispatchQueue.main.async {
NotificationCenter.default.post(name: .didFindRelevantTransactions, object: self, userInfo: ["transactions": transactions])
}
// Deduplicate the new tags.
var newTagObjectIDs = [NSManagedObjectID]()
let tagEntityName = Tag.entity().name
for transaction in transactions where transaction.changes != nil {
for change in transaction.changes!
where change.changedObjectID.entity.name == tagEntityName && change.changeType == .insert {
newTagObjectIDs.append(change.changedObjectID)
}
}
if !newTagObjectIDs.isEmpty {
deduplicateAndWait(tagObjectIDs: newTagObjectIDs)
}
// Update the history token using the last transaction.
lastHistoryToken = transactions.last!.token
}
}
}
// MARK: - Deduplicate tags
extension CoreDataStack {
/**
Deduplicate tags with the same name by processing the persistent history, one tag at a time, on the historyQueue.
All peers should eventually reach the same result with no coordination or communication.
*/
private func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID]) {
// Make any store changes on a background context
let taskContext = persistentContainer.backgroundContext()
// Use performAndWait because each step relies on the sequence. Since historyQueue runs in the background, waiting won’t block the main queue.
taskContext.performAndWait {
tagObjectIDs.forEach { tagObjectID in
self.deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)
}
// Save the background context to trigger a notification and merge the result into the viewContext.
taskContext.save(with: .deduplicate)
}
}
/**
Deduplicate a single tag.
*/
private func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {
guard let tag = performingContext.object(with: tagObjectID) as? Tag,
let tagName = tag.name else {
fatalError("###\(#function): Failed to retrieve a valid tag with ID: \(tagObjectID)")
}
// Fetch all tags with the same name, sorted by uuid
let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: Schema.Tag.uuid.rawValue, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "\(Schema.Tag.name.rawValue) == %@", tagName)
// Return if there are no duplicates.
guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {
return
}
print("###\(#function): Deduplicating tag with name: \(tagName), count: \(duplicatedTags.count)")
// Pick the first tag as the winner.
let winner = duplicatedTags.first!
duplicatedTags.removeFirst()
remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)
}
/**
Remove duplicate tags from their respective posts, replacing them with the winner.
*/
private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {
duplicatedTags.forEach { tag in
defer { performingContext.delete(tag) }
guard let posts = tag.posts else { return }
for case let post as Post in posts {
if let mutableTags: NSMutableSet = post.tags?.mutableCopy() as? NSMutableSet {
if mutableTags.contains(tag) {
mutableTags.remove(tag)
mutableTags.add(winner)
}
}
}
}
}
}
我正在尝试使用 CoreData 和 QueryGenerationTokens 运行示例项目。该项目的本质是在计时器上提交对后台上下文的更改(模拟来自服务器的更改),直到对 UI 采取操作(例如,按下按钮).
目前,我在后台上下文中保存了更改(每 5 秒添加并保存一个实体)并且它们自动进入视图上下文(正如预期的那样,.automaticallyMergesChangesFromParent 设置为 true)。 出现问题的地方,我在当前查询生成令牌发生任何这些更改之前固定视图上下文。我希望视图不会随着添加的背景项目而更新,但会随着它们而更新。所以查询生成令牌似乎没有效果?
我想到的一些可能的问题:
- 我从 Apple 找到的唯一 example 没有显示他们将它与获取结果控制器一起使用(我在 SwiftUI 中使用
@FetchRequest
,我'几乎完全可以肯定本质上是一样的),所以这可能会产生影响? - .automaticallyMergeChangesFromParent 不应使用,我应该尝试合并策略,但这似乎也不起作用,从概念上讲,查询生成令牌似乎应该与此一起使用并固定到生成,无论合并。
视图代码 - 处理从视图上下文加载数据
// Environment object before fetch request necessary
// Passed in wherever main view is instantiated through .environment()
@Environment(\.managedObjectContext) var managedObjectContext
// Acts as fetched results controller, loading data automatically into items upon the managedObjectContext updating
// ExampleCoreDataEntity.retrieveItemsFetchRequest() is an extension method on the entity to easily get a fetch request for the type with sorting
@FetchRequest(fetchRequest: ExampleCoreDataEntity.retrieveItemsFetchRequest()) var items: FetchedResults<ExampleCoreDataEntity>
var body: some View {
NavigationView {
// Button to refresh and bring in changes
Button(
action: {
do {
try self.managedObjectContext.setQueryGenerationFrom(.current)
self.managedObjectContext.refreshAllObjects()
} catch {
print(error.localizedDescription)
}
},
label: { Image(systemName: "arrow.clockwise") }
)
// Creates a table of items sorted by the entity itself (entities conform to Hashable)
List(self.items, id: \.self) { item in
Text(item.name ?? "")
}
}
}
SceneDelegate 中的代码(SwiftUI 应用程序启动的地方)我还初始化了 CoreData 所需的内容:
// Setup and pass in environment of managed object context to main view
// via extension on persistent container that sets up CoreData stack
let managedObjectContext = NSPersistentContainer.shared.viewContext
do {
try managedObjectContext.setQueryGenerationFrom(.current)
} catch {
print(error.localizedDescription)
}
let view = MainView().environment(\.managedObjectContext, managedObjectContext)
// Setup background adding
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(backgroundCode), userInfo: nil, repeats: true)
// Setup window and pass in main view
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: view)
后台添加数据功能:
@objc func backgroundCode() {
ExampleCoreDataEntity.create(names: ["background object"], in: backgroundContext, shouldSave: true)
}
NSPersistentContainer 的设置:
extension NSPersistentContainer {
private struct SharedContainerStorage {
static let container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Core_Data_Exploration")
container.loadPersistentStores { (description, error) in
guard error == nil else {
assertionFailure("CoreData: Unresolved error \(error!.localizedDescription)")
return
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
return container
}()
}
static var shared: NSPersistentContainer {
return SharedContainerStorage.container
}
}
Create/Read/Update/Delete 在实体上运行:
extension ExampleCoreDataEntity {
static func retrieveItemsFetchRequest() -> NSFetchRequest<ExampleCoreDataEntity> {
let request: NSFetchRequest<ExampleCoreDataEntity> = ExampleCoreDataEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \ExampleCoreDataEntity.creationDate, ascending: false)]
return request
}
static func create(names: [String], in context: NSManagedObjectContext, shouldSave save: Bool = false) {
context.perform {
names.forEach { name in
let item = ExampleCoreDataEntity(context: context)
item.name = name
item.creationDate = Date()
item.identifier = UUID()
}
do {
if save {
try context.save()
}
} catch {
// print error
}
}
}
func delete(in context: NSManagedObjectContext, shouldSave save: Bool = false) {
context.perform {
let name = self.name ?? "an item"
context.delete(context.object(with: self.objectID))
do {
if save {
try context.save()
}
} catch {
// print error
}
}
}
}
问题是 container.viewContext.automaticallyMergesChangesFromParent = true
在使用查询生成标记时,属性 不能设置为 true。我回到这个问题并在 automaticallyMergesChangesFromParent
:
NSManagedObjectContext
的 header 中找到了这个
Setting this property to YES when the context is pinned to a non-current query generation is not supported.
让它工作的一般流程如下:
- 将查询生成令牌设置为
.current
- 在视图上下文中调用
.refreshAllObjects()
- 在获取的结果控制器上调用
.performFetch()
这最后一部分违背了我在原始问题中使用的代码 @FetchRequest
- 目前,我想不出一种方法来手动重新获取它似乎不是非常 hacky。为了解决这个问题,我创建了一个中间存储 class,其中包含一个采用其委托协议的 FetchedResultsController
。该商店还采用了 ObservableObject
,它允许 SwiftUI 视图在采用商店的 ObservableObject
中调用 objectWillChange.send()
时监听其更改。
在问题中链接到的文档中,您会看到它说:
"在任何固定的上下文上调用 save()、reset()、mergeChangesFromContextDidSaveNotification: 或 mergeChangesFromRemoteContextSave(:intoContexts:) 将自动将其推进到操作的最新版本,然后重置其查询生成到 currentQueryGenerationToken。"
您看到后台保存的变化的原因是 automaticallyMergesChangesFromParent
只是为了方便 mergeChangesFromContextDidSaveNotification
所以你们这一代人在进步。
仅供参考,这是另一个使用查询生成的示例项目 - Synchronizing a Local Store to the Cloud
这里是相关代码:
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
A class to set up the Core Data stack, observe Core Data notifications, process persistent history, and deduplicate tags.
*/
import Foundation
import CoreData
// MARK: - Core Data Stack
/**
Core Data stack setup including history processing.
*/
class CoreDataStack {
/**
A persistent container that can load cloud-backed and non-cloud stores.
*/
lazy var persistentContainer: NSPersistentContainer = {
// Create a container that can load CloudKit-backed stores
let container = NSPersistentCloudKitContainer(name: "CoreDataCloudKitDemo")
// Enable history tracking and remote notifications
guard let description = container.persistentStoreDescriptions.first else {
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (_, error) in
guard let error = error as NSError? else { return }
fatalError("###\(#function): Failed to load persistent stores:\(error)")
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
// Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container)
return container
}()
/**
Track the last history token processed for a store, and write its value to file.
The historyQueue reads the token when executing operations, and updates it after processing is complete.
*/
private var lastHistoryToken: NSPersistentHistoryToken? = nil {
didSet {
guard let token = lastHistoryToken,
let data = try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true) else { return }
do {
try data.write(to: tokenFile)
} catch {
print("###\(#function): Failed to write token data. Error = \(error)")
}
}
}
/**
The file URL for persisting the persistent history token.
*/
private lazy var tokenFile: URL = {
let url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create persistent container URL. Error = \(error)")
}
}
return url.appendingPathComponent("token.data", isDirectory: false)
}()
/**
An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
*/
private lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
/**
The URL of the thumbnail folder.
*/
static var attachmentFolder: URL = {
var url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
url = url.appendingPathComponent("attachments", isDirectory: true)
// Create it if it doesn’t exist.
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create thumbnail folder URL: \(error)")
}
}
return url
}()
init() {
// Load the last token from the token file.
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
print("###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
}
}
}
}
// MARK: - Notifications
extension CoreDataStack {
/**
Handle remote store change notifications (.NSPersistentStoreRemoteChange).
*/
@objc
func storeRemoteChange(_ notification: Notification) {
print("###\(#function): Merging changes from the other persistent store coordinator.")
// Process persistent history to merge changes from other coordinators.
historyQueue.addOperation {
self.processPersistentHistory()
}
}
}
/**
Custom notifications in this sample.
*/
extension Notification.Name {
static let didFindRelevantTransactions = Notification.Name("didFindRelevantTransactions")
}
// MARK: - Persistent history processing
extension CoreDataStack {
/**
Process persistent history, posting any relevant transactions to the current view.
*/
func processPersistentHistory() {
let taskContext = persistentContainer.newBackgroundContext()
taskContext.performAndWait {
// Fetch history received from outside the app since the last token
let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
historyFetchRequest.predicate = NSPredicate(format: "author != %@", appTransactionAuthorName)
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
request.fetchRequest = historyFetchRequest
let result = (try? taskContext.execute(request)) as? NSPersistentHistoryResult
guard let transactions = result?.result as? [NSPersistentHistoryTransaction],
!transactions.isEmpty
else { return }
// Post transactions relevant to the current view.
DispatchQueue.main.async {
NotificationCenter.default.post(name: .didFindRelevantTransactions, object: self, userInfo: ["transactions": transactions])
}
// Deduplicate the new tags.
var newTagObjectIDs = [NSManagedObjectID]()
let tagEntityName = Tag.entity().name
for transaction in transactions where transaction.changes != nil {
for change in transaction.changes!
where change.changedObjectID.entity.name == tagEntityName && change.changeType == .insert {
newTagObjectIDs.append(change.changedObjectID)
}
}
if !newTagObjectIDs.isEmpty {
deduplicateAndWait(tagObjectIDs: newTagObjectIDs)
}
// Update the history token using the last transaction.
lastHistoryToken = transactions.last!.token
}
}
}
// MARK: - Deduplicate tags
extension CoreDataStack {
/**
Deduplicate tags with the same name by processing the persistent history, one tag at a time, on the historyQueue.
All peers should eventually reach the same result with no coordination or communication.
*/
private func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID]) {
// Make any store changes on a background context
let taskContext = persistentContainer.backgroundContext()
// Use performAndWait because each step relies on the sequence. Since historyQueue runs in the background, waiting won’t block the main queue.
taskContext.performAndWait {
tagObjectIDs.forEach { tagObjectID in
self.deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)
}
// Save the background context to trigger a notification and merge the result into the viewContext.
taskContext.save(with: .deduplicate)
}
}
/**
Deduplicate a single tag.
*/
private func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {
guard let tag = performingContext.object(with: tagObjectID) as? Tag,
let tagName = tag.name else {
fatalError("###\(#function): Failed to retrieve a valid tag with ID: \(tagObjectID)")
}
// Fetch all tags with the same name, sorted by uuid
let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: Schema.Tag.uuid.rawValue, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "\(Schema.Tag.name.rawValue) == %@", tagName)
// Return if there are no duplicates.
guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {
return
}
print("###\(#function): Deduplicating tag with name: \(tagName), count: \(duplicatedTags.count)")
// Pick the first tag as the winner.
let winner = duplicatedTags.first!
duplicatedTags.removeFirst()
remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)
}
/**
Remove duplicate tags from their respective posts, replacing them with the winner.
*/
private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {
duplicatedTags.forEach { tag in
defer { performingContext.delete(tag) }
guard let posts = tag.posts else { return }
for case let post as Post in posts {
if let mutableTags: NSMutableSet = post.tags?.mutableCopy() as? NSMutableSet {
if mutableTags.contains(tag) {
mutableTags.remove(tag)
mutableTags.add(winner)
}
}
}
}
}
}