Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct sync when object deleted #63

Open
Metman opened this issue Dec 15, 2017 · 5 comments
Open

Correct sync when object deleted #63

Metman opened this issue Dec 15, 2017 · 5 comments

Comments

@Metman
Copy link

Metman commented Dec 15, 2017

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 Metman closed this as completed Dec 15, 2017
@Metman Metman reopened this Dec 15, 2017
@Metman
Copy link
Author

Metman commented Dec 15, 2017

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
Copy link

DJ-Glock commented Dec 19, 2017

@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
Copy link
Author

Metman commented Dec 19, 2017

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

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

@DJ-Glock
Copy link

@Metman Can you give me your email to discuss?

@Metman
Copy link
Author

Metman commented Dec 19, 2017

Yes.
metallomaniac666@me.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants