How to use Ensembles Framework in Swift Project
Asked Answered
W

0

8

I have a project which already uses Core Data. I have added support for the iPad, but now will need to use iCloud & Core Data to sync the data.

I came across Ensembles, it seems like an easy and robust framework to add to my project. Found here: https://github.com/drewmccormack/ensembles

However there are no Swift example projects with the Ensembles project so have attempted to do it myself. Here are the steps I have taken,

Step 1

Manually add Ensembles to iOS projects.

Step 2

Create new CoreDataStack using existing persistent store .sql file.

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()
                    }
                }
            })
        }
    }
}

Step 3

Update App Delegate to sync and add notifications

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)
}

Step 4

Add Notifications to project to refresh 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
}

Step 5

Watch the magic happen.. Unfortunately there is no magic atm. The new CoreDataStack works fine, I can save and retrieve data from the persistent store.

I have two devices logged into the same iCloud account, and neither data is shared to the other device.

When deleting the app and reinstalling the data is not retrieved from the iCloud and saved to the persistent store.

I do get the following NSLog when 'sometimes' saving data or loading up the app.

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

This is the outcome for the following appDelegate notification function

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

The notification is sent from this function in the CoreDataStack for when changes are merged.

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)
        })
    }
}

So everything seems like it should be working fine. I get no errors but the data is not syncing. I cannot tell if the problem is the data backing up to iCloud or retrieving from iCloud and merging with the persistent store. All I can tell is that data is not shared between devices using the same iCloud account and the app does not actually restore the data from the iCloud when reinstalling the app.

Woodprint answered 6/4, 2016 at 21:33 Comment(4)
There is a Swift example as part of the Ensembles 2 project, but that is commercial. I am the developer, so I will post a link to the sample project here: dropbox.com/s/qflbtokbe24noy8/…Miner
@DrewMcCormack: Can Ensembles be integrated with custom API (own back-end) ?Dumbwaiter
Probably not the way you are thinking. You can't attach it to your DB and share it with a web service, which is probably what you want. You can build a custom backend/server, but that is effectively a file server. The files that are used by Ensembles are more like Git commits, and not designed to consumed by a standard web service or used to populate tables in a DB.Miner
is there a way to add ensembles if you use a Persistence Container? Rather than a Persistent Store Coordinator? AKA iOS 10+Foamflower

© 2022 - 2024 — McMap. All rights reserved.