paulw11 / Seam3

Cloudkit based persistent store for Core Data
Other
209 stars 25 forks source link

Correct sync when object deleted #63

Open Metman opened 6 years ago

Metman commented 6 years ago

Hello Paul! Thank you for library :)

I have problem with sync. If I delete record on first device and will open app on second device, my app will crash. Because Fetched Result Controller get deleted object, but object empty, all property is nil.

My AppDelegate :

import UIKit
import CoreData
import CloudKit
import Seam3
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    var smStore: SMStore?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        let notificationOptions = UIUserNotificationSettings(types: [.alert], categories: nil)
        application.registerUserNotificationSettings(notificationOptions)
        application.registerForRemoteNotifications()

        // Seam3 configuring
        if #available(iOS 10.0, *) {
            let storeDescriptionType = self.persistentContainer.persistentStoreCoordinator.persistentStores.first?.type
            if storeDescriptionType == SMStore.type {
                print("Store is SMStore")
                self.smStore = self.persistentContainer.persistentStoreCoordinator.persistentStores.first as? SMStore
            }
        } else {
            let storeDescriptionType = self.managedObjectContext.persistentStoreCoordinator?.persistentStores.first?.type
            if storeDescriptionType == SMStore.type {
                print("Store is SMStore")
                print()
                self.smStore = self.managedObjectContext.persistentStoreCoordinator?.persistentStores.first as? SMStore
            }
        }

        // Save sync status after sync is finished
        NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: SMStoreNotification.SyncDidFinish), object: nil, queue: nil) { notification in

            print("SyncDidFinish")
            if notification.userInfo != nil {
                let appDelegate = UIApplication.shared.delegate as! AppDelegate
                appDelegate.smStore?.triggerSync(complete: true)
            }

            self.viewContext.refreshAllObjects()
        }

        validateCloudKitAndSync {
            print("validateComplete")
        }

        NotificationCenter.default.addObserver(self, selector: #selector(merge(notification:)), name: NSNotification.Name.NSManagedObjectContextDidSave, object: nil)

        return true
    }

    func merge(notification: Notification) {
        self.smStore?.triggerSync()
}

       // MARK: - Remote notifications  
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        print("Registered for remote notifications")
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Remote notification registration failed")
    }

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
        print("didReceiveRemoteNotification")
        self.smStore?.handlePush(userInfo: userInfo)
    }

    // Function to start sync
    func validateCloudKitAndSync(_ completion:@escaping (() -> Void)) {
        self.smStore?.verifyCloudKitConnectionAndUser() { (status, user, error) in
            guard status == .available, error == nil else {
                NSLog("Unable to verify CloudKit Connection \(error!)")
                return
            }
            guard let currentUser = user else {
                NSLog("No current CloudKit user")
                return
            }
            let previousUser = UserDefaults.standard.string(forKey: "CloudKitUser")
            if  previousUser != currentUser && previousUser != nil {
                do {
                    print("New user")
                    try self.smStore?.resetBackingStore()
                } catch {
                    NSLog("Error resetting backing store - \(error.localizedDescription)")
                    return
                }
            }
            UserDefaults.standard.set(currentUser, forKey:"CloudKitUser")
            self.smStore?.triggerSync(complete: true)
            completion()
        }
    }

    // Lifecycle functions
    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
        // Saves changes in the application's managed object context before the application terminates.
        self.saveContext()
    }

    // MARK: - Core Data stack
    @available(iOS 10.0, *)
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "WorkApp")
        let persistentStoreCoordinator = container.persistentStoreCoordinator

        // Preparing URL
        let applicationDocumentsDirectory: URL = {
            let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
            return urls[urls.count-1]
        }()

        // Initializing Seam3
        SMStore.registerStoreClass()
        SMStore.syncAutomatically = false

        let newURL = applicationDocumentsDirectory.appendingPathComponent("WorkApp.sqlite")

        // Check if SQLite store has been already migrated by checking if CafeManagerSeam3.sqlite exists.
        let seamStoreExists = FileManager.default.fileExists(atPath: newURL.path)

        if seamStoreExists {
            // If exists, then use it because it has been already migrated to Seam3 storage
            print("Already migrated, using \(newURL)")

            let storeDescription = NSPersistentStoreDescription(url: newURL)
            storeDescription.type = SMStore.type
            storeDescription.setOption("iCloud.org.maxphil.payslip" as NSString, forKey: SMStore.SMStoreContainerOption)
            storeDescription.setOption(NSNumber(value:SMSyncConflictResolutionPolicy.clientTellsWhichWins.rawValue), forKey:SMStore.SMStoreSyncConflictResolutionPolicyOption)
            container.persistentStoreDescriptions=[storeDescription]

            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {
                    fatalError("Unresolved error \(error), \(error.userInfo)")
                }
            })
            print("Current persistent store descriptions are: \(container.persistentStoreDescriptions)")
            return container

        } else {
            // If does not exist, then migrate old storage to Seam3.
            print("Not yet migrated, migrating to \(newURL)")

            // Loadig default store
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {
                    fatalError("Failed to load default store \(error), \(error.userInfo)")
                }
            })
            let defaultPersistentStore = container.persistentStoreCoordinator.persistentStores.last
            print("Default store is located here: \(defaultPersistentStore!.url!)")

            // Migrating default store to new Seam store
            let options: [String : Any] = [SMStore.SMStoreContainerOption: "iCloud.org.maxphil.payslip"]
            do {
                try persistentStoreCoordinator.migratePersistentStore(defaultPersistentStore!, to: newURL, options: nil, withType:SMStore.type)
            }
            catch {
                fatalError("Failed to migrate to Seam store: \(error)")
            }
            return container
        }
    }()

    // MARK: - Core Data stack for iOS 9 (8+)
    lazy var managedObjectContext: NSManagedObjectContext = {
        var managedObjectModel: NSManagedObjectModel = {
            let modelURL = Bundle.main.url(forResource: "WorkApp", withExtension: "momd")!
            return NSManagedObjectModel(contentsOf: modelURL)!
        }()

        // Preparing URL
        var applicationDocumentsDirectory: URL = {
            let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
            return urls[urls.count-1]
        }()

        // Initializing Seam3
        SMStore.registerStoreClass()
        SMStore.syncAutomatically = false

        let oldURL = applicationDocumentsDirectory.appendingPathComponent("SingleViewCoreData.sqlite")
        let newURL = applicationDocumentsDirectory.appendingPathComponent("WorkApp.sqlite")

        // Check if SQLite store has been already migrated by checking if CafeManagerSeam3.sqlite exists.
        let seamStoreExists = FileManager.default.fileExists(atPath: newURL.path)

        if seamStoreExists {
            // If exists, then use it because it has been already migrated to Seam3 storage
            print("Already migrated, using \(newURL)")

            var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
                let coordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
                do
                {
                    let options: [String : Any] = [NSMigratePersistentStoresAutomaticallyOption: true,
                                                   NSInferMappingModelAutomaticallyOption: true,
                                                   SMStore.SMStoreContainerOption: "iCloud.org.maxphil.payslip",
                                                   SMStore.SMStoreSyncConflictResolutionPolicyOption: NSNumber(value:SMSyncConflictResolutionPolicy.clientTellsWhichWins.rawValue)]
                    try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: newURL, options: options)
                } catch {
                    NSLog("Error initializing smStore for iOS 8+ - \(error.localizedDescription)")
                }
                return coordinator
            }()

            var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
            managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
            return managedObjectContext
        }
        else {
            // If does not exist, then migrate old storage to Seam3.
            print("Not yet migrated, migrating to \(newURL)")

            var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
                let coordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)

                // Initializing old store
                do {
                    let options: [String : Any] = [NSMigratePersistentStoresAutomaticallyOption: true,
                                                   NSInferMappingModelAutomaticallyOption: true]
                    let oldStore = try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: oldURL, options: options)

                    // Migrate old store to Seam3
                    do
                    {
                        let options: [String : Any] = [NSMigratePersistentStoresAutomaticallyOption: true,
                                                       NSInferMappingModelAutomaticallyOption: true,
                                                       SMStore.SMStoreContainerOption: "iCloud.org.maxphil.payslip",
                                                       SMStore.SMStoreSyncConflictResolutionPolicyOption: NSNumber(value:SMSyncConflictResolutionPolicy.clientTellsWhichWins.rawValue)]
                        try coordinator.migratePersistentStore(oldStore, to: newURL, options: options, withType: SMStore.type)
                    } catch {
                        NSLog("Error initializing smStore for iOS 9 - \(error.localizedDescription)")
                    }
                } catch {
                    NSLog("Error initializing default NSSQLiteStore for iOS 9 - \(error.localizedDescription)")
                }

                return coordinator
            }()

            var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
            managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
            return managedObjectContext
        }
    }()

    // MARK: View Context for all iOS versions
    var viewContext: NSManagedObjectContext {
        if #available(iOS 10.0, *) {
            return self.persistentContainer.viewContext
        } else {
            return self.managedObjectContext
        }
    }

    // MARK: Core Data Saving context for all iOS versions
    func saveContext () {
        let context = viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}

I have this code for refresh TableView:

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: SMStoreNotification.SyncDidFinish), object: nil, queue: nil) { notification in
            self.reloadFRC()
        }

 func reloadFRC() {
        do {  
              try fetchedResultController.performFetch()
        }
        catch {
            print(error)
        }
   }
Metman commented 6 years ago

Maybe FRC update table view incorrect. I have 2 section in FetchedResultController. After deleting on first device, on second device app crashed and i see new section on logs.

Sorry for my bad English.

DJ-Glock commented 6 years ago

@Metman Hi. Offtopic, but I can see you have taken my code for migration. There is critical error for iOS 8-9: if seamStoreExists { ... try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: newURL, options: options)

There should be SMStore.type instead of NSSQLiteStoreType.

Metman commented 6 years ago

Hello Denis. Thank you :). I edided my code. Do you using FetchedResultController? Sync correct after deleting?

PS Предположу что вы говорите по-русски. После удаления новая секция создается с пустым объектом :)

DJ-Glock commented 6 years ago

@Metman Can you give me your email to discuss?

Metman commented 6 years ago

Yes. metallomaniac666@me.com