8

我有一个已经使用 Core Data 的项目。我添加了对 iPad 的支持,但现在需要使用 iCloud 和 Core Data 来同步数据。

我遇到了 Ensembles,它似乎是一个简单而强大的框架,可以添加到我的项目中。在这里找到:https ://github.com/drewmccormack/ensemble

然而,Ensembles 项目没有 Swift 示例项目,所以我尝试自己做。这是我采取的步骤,

步骤1

手动将 Ensembles 添加到 iOS 项目。

第2步

使用现有的持久存储 .sql 文件创建新的 CoreDataStack。

import UIKit
import CoreData

class CoreDataStack: NSObject, CDEPersistentStoreEnsembleDelegate {

    static let defaultStack = CoreDataStack()

    var ensemble : CDEPersistentStoreEnsemble? = nil
    var cloudFileSystem : CDEICloudFileSystem? = nil

    // MARK: - Core Data stack

    lazy var storeName : String = {
        return NSBundle.mainBundle().objectForInfoDictionaryKey(kCFBundleNameKey as String) as! String
    }()


    lazy var sqlName : String = {
        return "SingleViewCoreData.sqlite"
    }()


    lazy var icloudStoreName : String = {
        return self.storeName + "CloudStore"
    }()

    lazy var storeDescription : String = {
        return "Core data stack of " + self.storeName
    }()

    lazy var iCloudAppID : String = {
        return "iCloud." + NSBundle.mainBundle().bundleIdentifier!
    }()


    lazy var modelURL : NSURL = {
        return NSBundle.mainBundle().URLForResource(self.storeName, withExtension: "momd")!
    }()

    lazy var storeDirectoryURL : NSURL = {
        var directoryURL : NSURL? = nil
        do {
            try directoryURL = NSFileManager.defaultManager().URLForDirectory(NSSearchPathDirectory.ApplicationSupportDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
            directoryURL = directoryURL!.URLByAppendingPathComponent(NSBundle.mainBundle().bundleIdentifier!, isDirectory: true)
        } catch {
            NSLog("Unresolved error: Application's document directory is unreachable")
            abort()
        }
        return directoryURL!
    }()

    lazy var storeURL : NSURL = {
        return self.storeDirectoryURL.URLByAppendingPathComponent(self.sqlName)
        //       return self.applicationDocumentsDirectory.URLByAppendingPathComponent(self.sqlName)
    }()

    lazy var applicationDocumentsDirectory: NSURL = {
        // The directory the application uses to store the Core Data store file. This code uses a directory named "com.dprados.CoreDataSpike" in the application's documents Application Support directory.
        let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
        return urls[urls.count-1]
    }()

    lazy var managedObjectModel: NSManagedObjectModel = {
        // The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
        let modelURL = NSBundle.mainBundle().URLForResource(self.storeName, withExtension: "momd")
        return NSManagedObjectModel(contentsOfURL: modelURL!)!
    }()

    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
        // The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
        // Create the coordinator and store

        let coordinator : NSPersistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)

        var options = [NSObject: AnyObject]()
        options[NSMigratePersistentStoresAutomaticallyOption] = NSNumber(bool: true)
        options[NSInferMappingModelAutomaticallyOption] = NSNumber(bool: true)

        do {
            try NSFileManager.defaultManager().createDirectoryAtURL(self.storeDirectoryURL, withIntermediateDirectories: true, attributes: nil)
        } catch {
            NSLog("Unresolved error: local database storage position is unavailable.")
            abort()
        }
        // Create the coordinator and store
        let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite")
        var failureReason = "There was an error creating or loading the application's saved data."
        do {
            try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)

        } catch {
            // Report any error we got.
            var dict = [String: AnyObject]()
            dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
            dict[NSLocalizedFailureReasonErrorKey] = failureReason

            dict[NSUnderlyingErrorKey] = error as! NSError
            let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
            // Replace this with code to handle the error appropriately.
            // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
            abort()
        }

        return coordinator
    }()

    lazy var managedObjectContext: NSManagedObjectContext = {
        // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
        let coordinator = self.persistentStoreCoordinator
        var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
        managedObjectContext.persistentStoreCoordinator = coordinator
        managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

        return managedObjectContext
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        if managedObjectContext.hasChanges {
            do {
                try managedObjectContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
                abort()
            }
        }
    }

    static func save() {
        CoreDataStack.defaultStack.saveContext()
    }

    func enableEnsemble() {
        CoreDataStack.defaultStack.cloudFileSystem = CDEICloudFileSystem(ubiquityContainerIdentifier: nil)
        CoreDataStack.defaultStack.ensemble = CDEPersistentStoreEnsemble(ensembleIdentifier: self.storeName, persistentStoreURL: self.storeURL, managedObjectModelURL: self.modelURL, cloudFileSystem: CoreDataStack.defaultStack.cloudFileSystem)
        CoreDataStack.defaultStack.ensemble!.delegate = CoreDataStack.defaultStack
    }

    func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, didSaveMergeChangesWithNotification notification: NSNotification!) {
        CoreDataStack.defaultStack.managedObjectContext.performBlockAndWait({ () -> Void in
            CoreDataStack.defaultStack.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
        })
        if notification != nil {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.02 * Double(NSEC_PER_MSEC))), dispatch_get_main_queue(), {
                NSLog("Database was updated from iCloud")
                CoreDataStack.defaultStack.saveContext()
                NSNotificationCenter.defaultCenter().postNotificationName("DB_UPDATED", object: nil)
            })
        }
    }

    func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, globalIdentifiersForManagedObjects objects: [AnyObject]!) -> [AnyObject]! {
        NSLog("%@", (objects as NSArray).valueForKeyPath("uniqueIdentifier") as! [AnyObject])
        return (objects as NSArray).valueForKeyPath("uniqueIdentifier") as! [AnyObject]
    }

    func syncWithCompletion(completion: (() -> Void)!) {

        if CoreDataStack.defaultStack.ensemble!.leeched {
            CoreDataStack.defaultStack.ensemble!.mergeWithCompletion({ (error:NSError?) -> Void in
                if error != nil && error!.code != 103 {
                    NSLog("Error in merge: %@", error!)
                } else if error != nil && error!.code == 103 {
                    self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
                } else {
                    if completion != nil {
                        completion()
                    }
                }
            })
        } else {
            CoreDataStack.defaultStack.ensemble!.leechPersistentStoreWithCompletion({ (error:NSError?) -> Void in
                if error != nil && error!.code != 103 {
                    NSLog("Error in leech: %@", error!)
                } else if error != nil && error!.code == 103 {
                    self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
                } else {
                    self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
                    if completion != nil {
                        completion()
                    }
                }
            })
        }
    }
}

第 3 步

更新 App Delegate 以同步和添加通知

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


    let _ : CoreDataStack = CoreDataStack.defaultStack

    //        Value.ValueTypeInManagedObjectContext(CoreDataStack.defaultStack.managedObjectContext)
    CoreDataStack.defaultStack.saveContext()

    CoreDataStack.defaultStack.enableEnsemble()

    // Listen for local saves, and trigger merges
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "localSaveOccured:", name: CDEMonitoredManagedObjectContextDidSaveNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "cloudDataDidDownload:", name:CDEICloudFileSystemDidDownloadFilesNotification, object:nil)

    CoreDataStack.defaultStack.syncWithCompletion(nil);

    return true
}

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.

    let identifier : UIBackgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
    CoreDataStack.defaultStack.saveContext()
    CoreDataStack.defaultStack.syncWithCompletion( { () -> Void in
        UIApplication.sharedApplication().endBackgroundTask(identifier)
    })
}

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 throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
    NSLog("Received a remove notification")
}

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.
    CoreDataStack.defaultStack.syncWithCompletion(nil)
}

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.
    CoreDataStack.defaultStack.syncWithCompletion(nil)
}

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.
    CoreDataStack.defaultStack.saveContext()
}

func localSaveOccured(notif: NSNotification) {
    NSLog("Local save occured")
    CoreDataStack.defaultStack.syncWithCompletion(nil)
}

func cloudDataDidDownload(notif: NSNotification) {
    NSLog("Cloud data did download")
    CoreDataStack.defaultStack.syncWithCompletion(nil)
}

第4步

向项目添加通知以刷新 UI

override func viewWillAppear(animated: Bool) {
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "cloudDataDidDownload:", name:"DB_UPDATED", object:nil)
   //cloudDataDidDownload refetches the entities and reload the table
}

第 5 步

观看魔术发生。不幸的是,没有魔术自动取款机。新的 CoreDataStack 工作正常,我可以从持久存储中保存和检索数据。

我有两台设备登录到同一个 iCloud 帐户,并且没有数据共享给另一台设备。

删除应用程序并重新安装时,不会从 iCloud 中检索数据并保存到持久存储中。

NSLog“有时”保存数据或加载应用程序时,我确实得到以下信息。

2016-04-06 13:17:37.101 APPNAME[435:152241] Cloud data did download

这是以下 appDelegate 通知函数的结果

func cloudDataDidDownload(notif: NSNotification) {
NSLog("Cloud data did download")
CoreDataStack.defaultStack.syncWithCompletion(nil)
}

当合并更改时,CoreDataStack 中的此函数会发送通知。

func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, didSaveMergeChangesWithNotification notification: NSNotification!) {
    CoreDataStack.defaultStack.managedObjectContext.performBlockAndWait({ () -> Void in
        CoreDataStack.defaultStack.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
    })
    if notification != nil {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.02 * Double(NSEC_PER_MSEC))), dispatch_get_main_queue(), {
            NSLog("Database was updated from iCloud")
            CoreDataStack.defaultStack.saveContext()
            NSNotificationCenter.defaultCenter().postNotificationName("DB_UPDATED", object: nil)
        })
    }
}

所以一切似乎都应该正常工作。我没有收到任何错误,但数据没有同步。我无法判断问题是数据备份到 iCloud 还是从 iCloud 检索并与持久存储合并。我所能说的是,数据不会在使用相同 iCloud 帐户的设备之间共享,并且在重新安装应用程序时,该应用程序实际上并没有从 iCloud 恢复数据。

4

0 回答 0