Transition to Core Data with MagicalRecord

Every time I start developing a brand new iOS application, I always promise myself:

"I will decide on the whole infrastucture at the beginning and everything will be perfectly organized."

Can you guess what ends up happening? Things start to get out of control and I can't keep my promise 80 percent of the time. Usually it's not even me who is causing the issue, but one way or another, (sh)it happens.

I usually try to follow the MVC pattern when I am developing. I am not saying that it is the perfect approach but I feel comfortable with it. It lets you define objects' roles and provide a good line of communication between them.

Modelling is one such crucial role for an object, and not just because of its ability to encapsulate data. Storing models is often extremely useful, and when we talk about storage, there is a very good solution provided by good old Apple: Core Data.

A few months ago, I started developing a stock trading application that had to provide real-time data flow, users with multiple accounts and calculations from raw prices based on account type. While developing the previous project, I had not used a centralized modelling infrastructure, and as the project progressed, I started having problems. There were separate notifications for each case and my main view controller had more than 15 notification listeners that were getting triggered by different parts of the whole data.

So I decided to be more careful and organized in the next one. Hence our use of Core Data in this project.

Core Data

Core Data is an object graph and persistence framework provided by Apple in OS X and iOS operating systems. It "interfaces directly with SQLite, insulating the developer from the underlying SQL" (Yes, we all use Wikipedia). You can organize and store your data by creating entities and attributes from an interface that Xcode provides.

I was always skeptical about Core Data. Never used, never even thought of using it. Core Data for me was like one of those distant relatives; I was aware of its existence, but I always found a reason to not visit it. Well, now here I am.

Implementation

Below you can see an example code for Core Data Stack Initialization in Swift:

guard let modelURL = NSBundle.mainBundle().URLForResource("DataModel", withExtension:"momd") else {
    fatalError("Error loading model from bundle")
}

guard let mom = NSManagedObjectModel(contentsOfURL: modelURL) else {
    fatalError("Error initializing mom from: \(modelURL)")
}

let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)

managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = psc

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)

  let docURL = urls[urls.endIndex - 1]

let storeURL = docURL.URLByAppendingPathComponent("DataModel.sqlite")
   do {
      try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
  } catch {
      fatalError("Error migrating store: \(error)")
  }
}

I am not saying this is too hard to do, but in my mind, I've already done too many things just to initialize the thing. It was kind of intimidating.

However, I later discovered this magical tool called MagicalRecord. What does it do? Here's MagicalRecord's approach for stack initialization:

MagicalRecord.setupAutoMigratingCoreDataStack()

Now that doesn't look too bad, does it? Simplification at its best.

Adaptation

Since I have never worked with pure Core Data before, I will not compare pure Core Data and MagicalRecord + Core Data, but show you how it's like using MagicalRecord to save some data.

Creating & Saving Entities

MagicalRecord.saveWithBlock({ (localContext) -> Void in
if let jsonArray = data?.jsonObjectRepresentation() as! [[String: AnyObject]]? {
    let productType = (account.type == AccountType.Demo) ? ProductType.Demo : ProductType.Real

    for jsonDict in jsonArray {
        let namePredicate = NSPredicate(format: "name == %@", argumentArray: [jsonDict["Name"]!])
        let typePredicate = NSPredicate(format: "typeRaw == %@", argumentArray: [productType.rawValue])

        let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [namePredicate, typePredicate])

        var product: Product? = Product.MR_findFirstWithPredicate(compoundPredicate,
            inContext: localContext)

        if (product == nil) {
            product = Product.MR_createEntityInContext(localContext)
        }

        product?.updateWith(info: jsonDict)
        product?.type = productType
    }
}
}, completion: { (contextDidSave, error) -> Void in
    completion(contextDidSave, error)
})

As you can see MR provides a class method which allow us to create and save entities conveniently. It runs in a background thread and lets us define a localContext where we can perform all our operations.

Fetching Entities

Fetching is a lot easier. For instance, here is how you can get all entities of one type:

let products = Product.MR_findAll()

And here how you can fetch entities with filters:

// Controller Property
private var productsResultsController: NSFetchedResultsController!

// NSFetchedResultsController Update Method
func updateProductsResultsController() {
guard let account = selectedAccount else {
    return
}

let predicates = NSMutableArray()

for product in (account.products)! {
    predicates.addObject(NSPredicate(format: "name == %@", argumentArray: [(product as! Product).name!]))
}

let compoundNamePredicates = NSCompoundPredicate(orPredicateWithSubpredicates: predicates as NSArray as! [NSPredicate])

let typePredicate = NSPredicate(format: "typeRaw == %@", argumentArray: [(account.typeRaw)!])

let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [typePredicate, compoundNamePredicates])

if productsResultsController == nil {
    productsResultsController = Product.MR_fetchAllSortedBy("name",
                        ascending: true,
                    withPredicate: compoundPredicate,
                          groupBy: nil,
                         delegate: self)
} else {
    productsResultsController.fetchRequest.predicate = compoundPredicate

    do {
        try productsResultsController.performFetch()
    } catch {
        print("An error occurred")
    }
 }
 }

// NSFetchedResultsControllerDelegate
extension ProductsViewController: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(controller: NSFetchedResultsController) {
    if controller == productsResultsController {
        self.tableView.reloadData()
    }
}
}

You can see that you are able to filter your data by using predicates easily, and once your copy of NSFetchedResultsController object is created once, all you need to do is update the predicate and perform another fetch when there is a change on your filtering parameters.

Summary

With MagicalRecord, you do not need worry about the boilerplate code, all you need to do is get the most out of your entities. It hides lots of low level code from you and makes your life a LOT easier while working with data.

Now you can have a centralized model infrastructure inside your application. You can finally stop checking for the update states of the data. If you are using the same data in different controllers and implement everything correctly, whenever an object gets updated, every related piece of code inside your code will be notified and you will not need to do anything else to update your views. Getting rid off all the boilerplate code and reducing the number of KVO notification usage gives you a chance to have a little bit more control over your classes.

Need more info? Have a look at these links:

For a detailed tutorial from scratch you can read MagicalRecord Tutorial for iOS on Ray Wenderlich's website.

This is also an interesting post written by Saul Mora in the early days of MagicalRecord's development.

And here is the project page: https://github.com/magicalpanda/MagicalRecord

I hope this gives you an idea and simplifies your life as much as it did mine.