I have an app that uses NSPersistentCloudKitContainer for iCloud Sync to users who want it and has worked for all platforms (iPhone/iPad/Mac). But now, trying to add Apple Watch support, I realize that I might have implemented CloudKit wrong this whole time.
Code:
final class CoreDataManager {
static let sharedManager = CoreDataManager()
private var observer: NSKeyValueObservation?
lazy var persistentContainer: NSPersistentContainer = {
setupContainer()
}()
private func setupContainer() -> NSPersistentContainer {
var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")
#if os(watchOS)
useCloudSync = true
#endif
let containerToUse: NSPersistentContainer?
if useCloudSync {
containerToUse = NSPersistentCloudKitContainer(name: "app")
} else {
containerToUse = NSPersistentContainer(name: "app")
}
guard let container = containerToUse, let description = container.persistentStoreDescriptions.first else {
fatalError("Could not get a container!")
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if !useCloudSync {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in }
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = "app"
container.viewContext.automaticallyMergesChangesFromParent = true
NotificationCenter.default.addObserver(self, selector: #selector(type(of: self).storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
return container
}
}
//MARK: - History token
private lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
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)")
}
}
}
private lazy var tokenFile: URL = {
let url = NSPersistentCloudKitContainer.defaultDirectoryURL().appendingPathComponent("app", 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)
}()
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: - Process History
@objc func storeRemoteChange(_ notification: Notification) {
// Process persistent history to merge changes from other coordinators.
historyQueue.addOperation {
self.processPersistentHistory()
}
}
func processPersistentHistory() {
let backContext = persistentContainer.newBackgroundContext()
backContext.performAndWait {
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
let result = (try? backContext.execute(request)) as? NSPersistentHistoryResult
guard let transactions = result?.result as? [NSPersistentHistoryTransaction], !transactions.isEmpty else {
print("No transactions from persistent history")
return
}
// Update the history token using the last transaction.
if let lastToken = transactions.last?.token {
lastHistoryToken = lastToken
}
}
}
What I've noticed:
I only have 10 items on my test device. When I boot up the watch app and look at the console, it looks like it's going through the entire history of every addition and deletion of items I've ever done, making it take a long time to download the 10 items that I actually have left.
I looked in my iCloud storage and found out that my app is taking up a lot of space (48 MB) when the 10 items are just entities with a few strings attached to them
Research:
I've done a lot of research and found that it could be from setting NSPersistentHistoryTrackingKey only when the user is not using iCloud Sync. But when I enable NSPersistentHistoryTrackingKey for iCloud users too, the watch app still takes a very long time to download the 10 items.
I know Core Data can be tricky, and changing persistentStoreDescriptions
or other attributes of the container can be an app-breaking bug to existing users. So I need something that works for new and existing users.
Ask:
Does anyone know how to fix this problem or have had similar issues? I've been trying to figure this out for almost a week now, and any help would be greatly appreciated!