I published my first app (Pain Logger) on the app store about two years ago. For the app, I had some very simple data requirements. Essentially I wanted to store the user’s data in a database using Core Data and then sync that to the cloud so it would be available to other devices the user owned. Seemed simple at the time, but in practice it wasn’t. Like many other developers, I found that syncing wasn’t all it was cracked up to be.
This post has a good history of what I and apparently everyone else was dealing with:
To get my app out I reluctantly decided to disable the syncing capability. My goal was to add that feature back in, once I learned more.
Fast forward two years and this issue has continued to be on my radar. I have read countless blogs and tried different solutions but none have quite given me what I was looking for.
I had two events recently that have re-sparked my interest again in this. First, I had a customer contact me about why my app didn’t have that ability. Second, I have a new app (written in Swift) that I have been working on for about three months now and it has the same basic database syncing requirements.
I first considered using Core Data on the client and CloudKit for the cloud support. I looked into it enough to know that I didn’t like the idea of having to marshal my objects stored on the client device into name value pairs to be stored using CloudKit and then having to do the reverse when getting it back. On the surface this looked like it would be a lot of code.
I next looked at using two third party packages, MagicalRecord and Ensembles. I actually got them working. The one thing that kept nagging at me was the amount of third party code I was using that abstracted my code away from the basic Core Data. Not to mention Ensembles has a free for version 1.0 but "you must pay for version 2.0/source code/book/updates" pricing model. That made me a little hesitant to say I really needed it.
About a month ago I started looking into using the Parse framework. This seemed very promising and I was up very quickly with syncing to their cloud service. However, my new app has a need to store data locally when not connected and I wanted to only enable the syncing capability if the user purchased that capability as an in-app purchase.
I started working with Parse’s anonymous user capability and local storage and it sort of worked, but it seemed weird to me in my use case that the user would pay for the in-app purchase to sync their device and then I would have to have them create an account (so it could be saved in Parse).
I thought of various schemes of creating that user behind the scenes but I’m not convinced that would work. One great advantage I see in the Parse solution is you as a developer have great access to the user’s data stored in the cloud including the actual User login information, so if in the future there is a problem you might be able to fix it. One downside of Parse was that it was free for the first tier but if the app usage started to pick up you could be forced to pay a monthly fee to keep up with the increased traffic. I don’t think that is necessarily a deal killer just something I wasn’t sure about.
One thing that had always nagged at me was in a session at WWDC last year one of the speakers alluded to how bad Core Data syncing was when it was first implemented (that was the time I was trying to get my first app out). The speaker assured the audience that a lot of work had gone into fixing those issues.
Sooo, long story short, I decided to relook at just using plain Core Data syncing to see if it really has been fixed. However, I still want to use MagicalRecord as it seems to be a good compromise between raw Core Data and abstracting all of it away. So I have embarked down this path. Here is what I have done so far:
Step 1: Adding an iCloud Persistent Store to Core Data
I setup my new project to use Cocoapods and pulled in the MagicalRecord library and wired up the BridgingHeader as I am doing all my new projects in Swift to force me to learn the language.
I created my data model. One thing of note, if you let XCode build the NSManagedObject subclasses of your data model and you check the box “Use scalar properties for primitive data types” your Date attributes will be declared as NSTimeIntervals (which wasn’t what I wanted as I wanted dates to be nullable even though I had declared the attribute to be Optional) Would be nice if you could do this on an individual attribute level.
Next was to begin setting up the Core Data stack using the documentation for MagicalRecord(MR) and comparing with the iCloud Core Data Programming Guide from Apple. My first hurdle was what MR setup method should I use and how the parameters map to Apple’s setup instructions.
I figured I needed to use the most configurable version of the MagicalRecord.setupCoreDataStackWithiCloudContainer method.
Unfortunately this method is not documented so I needed to figure out what each of the parameters are and how they map to the underlying Core Data implementation. I have come up with the following definitions for each:
- containerID - This eventually gets mapped to bucketName in the MagicalRecord code. For iCloud support it must begin with “iCloud.” and I just appended my bundle identifier to that. It is used to create the URL for the ubiquity container. In Apple’s documentation this is used to create the storeURL. MagicalRecord will create what it calls the cloudURL with this and then attach that to the NSPersistentStoreUbiquitousContentURLKey in the options dictionary.
- contentNameKey - This is the name of the persistent store in the ubiquity container. If you use a name with periods you will get an error. After googling this a bit and trying things out I found that if I just used {my app name}_DataStore that would work fine. This value is used for the value to the NSPersistentStoreUbiquitousContentNameKey in Apple’s documentation.
- localStoreName - This is the name of the database as stored on the user’s device. MagicalRecord will use this to create NSPersistentStore for the local database.
- pathSubcomponent - This can be nil. I presume this is used in cases where you might want to store different stores under the same top-level ubiquity container. Since my app was one database I didn’t need it.
- completion - This is a completion block which is called after the default persistent store has been set. Note, this appears to be called before the iCloud container is setup.
After setting all this up I ran my app and as is mentioned in Apple’s documentation (the first Checkpoint) I saw in the log the two log messages, first using the local store and then not using the local store.
Some other notes to this point.
- When setting up my entitlement it was important to let XCode create one for my app. Essentially what I did was turn on the iCloud capability ensure Key-value Storage and iCloud Documents were checked and choose to Use default container and ensured I had my app’s iCloud container (remember it must start with iCloud.) listed and checked in the list.
- When testing this on the iOS Simulator I found I had to be logged into a valid iCloud account. Yeah I know, that seems obvious, but you try a lot of things when debugging.
After all of this here is the code I ended up with for step one.
var bundleIdentifier:String = ""
if let infoDictionary:NSDictionary = NSBundle.mainBundle().infoDictionary {
bundleIdentifier = infoDictionary.objectForKey(kCFBundleIdentifierKey) as String
}
let containerId = "iCloud.\(bundleIdentifier)"
let contentNameKey = "GoalTrak_DataStore"
MagicalRecord.setupCoreDataStackWithiCloudContainer(
containerId,
contentNameKey: contentNameKey,
localStoreNamed: STORE_NAME,
cloudStorePathComponent: nil,
completion: {_ in
NSLog("Finished setting up CoreData stack")
})
Two notes about the code. I chose to build the containerId by getting the bundle identifier from the mainBundle and appending “iCloud.” on the front of it. I could of just hard coded it.
Also the STORE_NAME is just a class level constant for the name of the local database.
Step 1 complete!!
Step 2: Reacting to iCloud Events
This step just seemed to be adding a listener for when the NSPersistentStoreCoordinatorStoresDidChangeNotification was fired. I added this line above the code I did in step one since the Apple documentation implied this should be done early.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "persistentStoreDidChange:", name: NSPersistentStoreCoordinatorStoresDidChangeNotification, object: nil)
Note: I did not listen to a specific NSPersistentStore as I wanted MagicalRecord to do as much of the heavy lifting as it could so at this point in the code I did not have a NSPersistentStore to listen to.
In the selector code I just printed out the notification’s userInfo:
func persistentStoreDidChange(notification:NSNotification) {
NSLog("Persistent store did change")
if let _info: [NSObject:AnyObject] = notification.userInfo {
for key in _info.keys {
NSLog(" Key: \(key) Value: \(_info[key])")
}
}
}
Step 2 complete!!
Step 3: iCloud Performs a One-Time Setup
At this point I realized that the Apple’s documentation is for a basic Core Data/iCloud setup and as such assumes you have direct access to the PersistentStoreCoordinator and uses the basic notifications. MagicalRecord abstracts a good bit of that away and rightly so as there are now more events that are more specific to the Core Data stack that MagicalRecord has setup. So I decided to refactor a bit and begin listening to some other more MagicalRecord specific events. Also in this step I had to setup my first saving of a record. This is accomplished with a simple algorithm of getting the ManagedObjectContext (MOC) for the object that needs to be saved and calling it’s MR_saveWithOptions method, passing in a completion block.
I know this is vague, but it is beginning to get app specific. My goal here is to document the events that MagicalRecord will send as an object gets saved.
One thing to get MagicalRecord to play nicely with Swift is you have to tell MagicalRecord what the entity name is, otherwise you will see errors when saving, like “entityForName: could not locate an entity named ‘{project name}.{class name}’ To fix this I had to add a class method to each NSManagedObject instance that looks like this (assuming your class name was Foobar)
class func MR_entityName() -> String {
return "Foobar"
}
You have to do this for each subclass of NSManagedObject that you have. Additionally you have to go into your Model and set each of the Entities class to a fully qualified name. So for instance using the example above and assuming your project was named MyProject you would enter MyProject.Foobar into the class attribute for the Foobar entity.
At this point I can create an object, and show a list of them. One thing that isn’t working is when the app first stands up, my initial list of objects is not shown in my UITableViewController. I think the problem here is the Core Data stack isn’t fully stood up before the viewDidLoad method in my initial view controller is called. For now I just added a refresh button. But I will add a notification this week and have my initial view controller listen and react to that.
That’s where I have stopped for now. My next steps are to load the app on two devices and follow the checkpoint steps in Apple’s documentation. I’m not convinced that I won’t need the Ensembles framework or I might even end up back using Parse. We’ll see as I continue traveling down this road. I think my main goal is to learn how this works, and have some type of service layer that I can apply to other apps as I build them. We’ll see how that goes.
No comments:
Post a Comment