Как использовать Ensembles Framework в проекте Swift

У меня есть проект, который уже использует Core Data. Я добавил поддержку iPad, но теперь мне нужно будет использовать iCloud & Core Data для синхронизации данных.

Я натолкнулся на Ensembles, кажется, что это простой и надежный фреймворк для добавления в мой проект. Найдено здесь: https://github.com/drewmccormack/ensembles

Однако нет примеров проектов Swift с проектом Ensembles, поэтому я попытался сделать это сам. Вот шаги, которые я предпринял,

Шаг 1

Вручную добавляйте ансамбли в проекты iOS.

Шаг 2

Создайте новый CoreDataStack, используя существующий файл постоянного хранилища.sql.

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

Обновите приложение 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

Добавить уведомления в проект для обновления пользовательского интерфейса

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 при переустановке приложения.

0 ответов

Другие вопросы по тегам