Categories
Swift Development

Core Data y CloudKit en las nuevas Apps MultiPlatafora de SwiftUI

En mi entrada anterior agregué Core Data a mi App MultiPlaforma ahora veamos si podemos agregar CloudKit para que se sincronice entre nuestros diferentes dispositivos.

Si no has leído el articulo anterior seria bueno que le dieras una leída

Paso 1

En primer lugar tenemos que añadir CloudKit a nuestro proyecto

Para eso nos dirigimos al archivo de nuestro proyecto donde podemos ver nuestros targets

Seleccionamos nuestro target y luego singning & Capabilities

Ahora seleccionamos el botón + capability y elegimos iCloud

Una vez añadido seleccionamos la casilla de CloudKit y en Containers elegimos alguno que ya tengamos o creamos uno nuevo.

Lo mismo para iOS y nuestros demás targets (Exceptuando los test )

También es necesario que en iOS añadamos BackGround Modes

y marcamos Remote Notifications

Paso 2

Así que ahora en nuestra clase PersistentCloudKitContainer.swift vamos cambiar nuestro container

En nuestra clase PersistentCloudKitContainer
let container = NSPersistentContainer(name: "ruleOfThree")

Por este que funciona con CloudKit

let container = NSPersistentCloudKitContainer(name: "ruleOfThree")

y lo mismo en el valor que retorna

...
public static var persistentContainer: NSPersistentContainer = {
...

por este otro

...
public static var persistentContainer: NSPersistentCloudKitContainer = {
...

De forma que nuestra clase queda de la siguiente forma

import CoreData
public class PersistentCloudKitContainer {
    // MARK: - Define Constants / Variables
    public static var context: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    // MARK: - Initializer
    private init() {}
    // MARK: - Core Data stack
    public static var persistentContainer: NSPersistentCloudKitContainer = {
        let container = NSPersistentCloudKitContainer(name: "ruleOfThree")
        
        guard let description = container.persistentStoreDescriptions.first else {
            fatalError("No descriptions found")
        }
        
        description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        
        return container
    }()
    // MARK: - Core Data Saving support
    public static func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}

Paso 3

Ahora el siguiente paso es dirigirnos a nuestro modelo de datos y marcar bajo Configurations > Default y en la vista del inspector marcamos used with CloudKit

Con eso ya podemos correr nuestros simuladores y probar sí están sincronizando. Para esto es necesario loggearnos en nuestros dos simuladores

Paso 4 (Extra)

Bien en este punto CloudKit y la sincronización ya funciona de manera correcta pero quiero añadir algunos ajustes extras para optimizar mis registros, evitar duplicados y poder reordenar los elementos.

así que regresamos a nuestra clase y añadimos el siguiente bloque de código

guard let description = container.persistentStoreDescriptions.first else {
            fatalError("No descriptions found")
}

description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

Con este código podemos recibir notificaciones cuando algo cambie en nuestro modelo de datos

NotificationCenter.default.addObserver(self, selector: #selector(self.processUpdate), name: .NSPersistentStoreRemoteChange, object: nil)

Ahora gracias a la recomendación de un profesional en twitter encontré una mejor forma de implementar la sincronización

Bien esta nueva implementación tiene muchas mejoras la principal es que ahora utilizamos @StateObject para manejar nuestro modelo de datos

Para eso es necesario hacer que nuestra clase se ajuste a ObservableObject

...
public class PersistentCloudKitContainer: ObservableObject {
...

Así que borramos

...  
// MARK: - Define Constants / Variables
    public static var context: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    // MARK: - Initializer
    private init() {}
...

y ahora nuestra clase queda de esta forma

import CoreData
public class PersistentCloudKitContainer: ObservableObject {
    
    // MARK: - Core Data stack
    lazy var persistentContainer: NSPersistentCloudKitContainer = {
        let container = NSPersistentCloudKitContainer(name: "ruleOfThree")
        
        guard let description = container.persistentStoreDescriptions.first else {
            fatalError("No descriptions found")
        }
        
        description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        NotificationCenter.default.addObserver(self , selector: #selector(processUpdate), name: .NSPersistentStoreRemoteChange, object: nil)
        
        return container
    }()
    
}

Ahora lo que sigue es modificar en nuestro @main


import SwiftUI
import CoreData

@main
struct ruleOfThreeApp: App {
    @StateObject var coreData = PersistentCloudKitContainer()
    var body: some Scene {
        WindowGroup {
            ContentView().environment(\.managedObjectContext, coreData.persistentContainer.viewContext)
        }
    }
}

y con eso podemos agregar funciones extras a nuestra clase por ejemplo en mi caso para poder reorder mis listas.

Referencias

En este caso base mi código en el siguiente tutorial