-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathPersistenceController+Deduplicate.swift
97 lines (90 loc) · 3.79 KB
/
PersistenceController+Deduplicate.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*
<samplecode>
<abstract>
An extension that wraps the methods related to deduplicating tags.
</abstract>
</samplecode>
*/
import CoreData
import CloudKit
// MARK: - Deduplicate tags
//
extension PersistenceController {
/**
Deduplicate tags that have a same name and are in the same CloudKit record zone, one tag at a time, on the historyQueue.
All peers should eventually reach the same result with no coordination or communication.
*/
//#-code-listing(deduplicateAndWait)
func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID])
//#-end-code-listing
{
/**
Make any store changes on a background context with the transaction author name of this app.
Use performAndWait to serialize the steps. historyQueue runs in the background so this won’t block the main queue.
*/
let taskContext = persistentContainer.newTaskContext()
taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
taskContext.performAndWait {
tagObjectIDs.forEach { tagObjectID in
deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)
}
taskContext.save(with: .deduplicateAndWait)
}
}
/**
Deduplicate one single tag.
*/
private func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {
/**
tag.name can be nil when the app inserts a tag and then ( before processing the insertion ) delete it.
In that case, silently ignore the deleted tag.
*/
guard let tag = performingContext.object(with: tagObjectID) as? Tag,
let tagName = tag.name else {
print("\(#function): Ignore a tag that was deleted: \(tagObjectID)")
return
}
/**
Fetch all tags with the same name, sorted by uuid, and return if there are no duplicates.
*/
let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: Tag.Schema.uuid.rawValue, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "\(Tag.Schema.name.rawValue) == %@", tagName)
guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {
return
}
/**
Filter out the tags that are not in the same CloudKit record zone.
Only tags that have the same name and are in the same record zone are duplicates.
The tag zone ID can be nil, which means it isn't a shared tag. The filter rule is still valid in that case.
*/
let tagZoneID = persistentContainer.recordID(for: tag.objectID)?.zoneID
duplicatedTags = duplicatedTags.filter {
self.persistentContainer.recordID(for: $0.objectID)?.zoneID == tagZoneID
}
guard duplicatedTags.count > 1 else {
return
}
/**
Pick the first tag as the winner.
*/
print("\(#function): Deduplicating tag with name: \(tagName), count: \(duplicatedTags.count)")
let winner = duplicatedTags.first!
duplicatedTags.removeFirst()
remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)
}
/**
Remove duplicate tags from their respective photos, replacing them with the winner.
*/
private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {
duplicatedTags.forEach { tag in
if let photoSet = tag.photos {
for case let photo as Photo in photoSet {
photo.removeFromTags(tag)
photo.addToTags(winner)
}
}
performingContext.delete(tag)
}
}
}