4

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!

fphelp
  • 1,544
  • 1
  • 15
  • 34
  • If there is a long history to process, this will take long only the 1st time. Then, you normally store a history token that prevents processing already processed history transactions again. Have you tried this, i.e. processing it once fully, store the history token in the user defaults, and then launch the app again? – Reinhard Männer Mar 22 '21 at 17:49
  • Hi @ReinhardMänner I just added my code for using the history token to process the new data. Does this seem right to you? This code is based off of some core data examples apple provided a long time ago – fphelp Mar 23 '21 at 21:59
  • Hi @fphelp: I did not check it in detail, but it seems to me that this corresponds to [Apple's suggestions](https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes). The only thing I did not understand there is the part at the end about purging history. [I ask a question about this](https://stackoverflow.com/q/64124940/1987726) by myself, unfortunately without an answer. – Reinhard Männer Mar 24 '21 at 20:07
  • @ReinhardMänner Thank you for the link information! Hopefully we'll get an answer soon about this! – fphelp Mar 27 '21 at 22:50

0 Answers0