Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Easy Core Data Fetching with Magical Record

by

Magical Record, created by Saul Mora, is an open-source library that makes working with Core Data easier and more elegant. The library was inspired by the active record pattern that is also found in Ruby on Rails. This tutorial will teach you how to use Magical Record in your own apps!

So, why Magical Record? If you've been developing for iOS or OS X for some time, chances are that you have had a taste of Core Data. This means that you know that it can be a bit cumbersome to set up a Core Data stack and, to be honest, working with Core Data can be a bit complex mainly due to its verbose syntax. For example, fetching data from a persistent store is quite verbose especially when compared with how a Ruby on Rails application handles this task.


Prerequisites

Even though Magical Record does make working with Core Data easier, it is key that you have a good understanding of Core Data if you decide to use it in a project. Despite its name, Magical Record doesn't perform any magic behind the scenes that makes Core Data work differently. In other words, if you run into problems at some point, it is crucial that you understand how Core Data works internally so you can fix any problems that might pop up along the way.


Requirements

Since the introduction of Magical Record, the requirements have been increased to iOS 5.0 (or higher) or OS X 10.7 (or higher). It is also worth mentioning that Magical Record supports ARC out of the box.


Magical Notes

The best way to show you what Magical Record has to offer is to create an application that makes use of this great library. It will show you how easy it is to get started with Magical Record and by starting from scratch it will show you what is involved in creating a project with Core Data and Magical Record. The application that we are about to create is a simple note taking application in which the user can create, update, and delete notes - a good candidate for Core Data.

Since you have read this far, I assume that you are familiar with iOS development and have a basic understanding of Core Data. In this article, I will mainly focus on the Core Data aspect of the application, which means that I will not discuss every code snippet in detail.


Step 1: Project Setup

Start by creating a new project based on the Single View Application template (figure 1) and name it Magical Notes (figure 2). Set the device family to iPhone and enable ARC by checking the check box labeled Use Automatic Reference Counting. We won't be using Storyboards or Unit Tests in this tutorial.

Magical Record and Core Data: Project Setup - Figure 1
Magical Record and Core Data: Project Configuration - Figure 2

Step 2: Add Magical Record

Since we will be using Core Data in this project, don't forget to link your project against the Core Data framework. Since this is a more advanced tutorial, I assume that you already know how to do this.

Adding the Magical Record library to your project doesn't require any magic. Download the latest version from GitHub, open the archive, and drag the folder named MagicalRecord into your Xcode project. Make sure to also copy the contents of the folder into your project by checking the check box labeled Copy items into destination group's folder (if needed) and don't forget to add the Magical Record library to the Magical Notes target (figure 3). An alternative approach to add Magical Record to your project is to use CocoaPods.

Magical Record and Core Data: Add Magical Record - Figure 3

To make use of Magical Record in your classes, we need to import one header file, CoreData+MagicalRecord.h. However, since we will be using Magical Record quite a bit in this tutorial it is much more convenient to move this import statement to your project's Prefix.pch file instead. This will make sure that Magical Record is available in every class of your project.

By default, all Magical Record methods are prefixed with MR_. You can omit the MR_ prefix by adding one extra line to your project's Prefix.pch file, #define MR_SHORTHAND. It is important that you add this line before importing the Magical Record header file.

//
// Prefix header for all source files of the 'Magical Notes' target in the 'Magical Notes' project
//

#import <Availability.h>

#ifndef __IPHONE_4_0
#warning "This project uses features only available in iOS SDK 4.0 and later."
#endif

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>

    #define MR_SHORTHAND
    #import "CoreData+MagicalRecord.h"
#endif

Step 3: Create a Core Data Model

Before setting up the Core Data stack, we need to create a Core Data model. The Core Data model for this project is simple as it consists of only one entity named Note. The Note entity has four attributes, date, title, body, and keywords. Title, body, and keywords are of type string, whereas date is of type date.

Start by creating a new Core Data model and name it MagicalNotes (figure 4). Create the Note entity and add the four attributes as outlined above (figure 5).

Magical Record and Core Data: Create a Core Data Model - Figure 4
Magical Record and Core Data: Create a Core Data Model - Figure 5

Before we move on, we need to create a custom NSManagedObject subclass for the Note entity. This is important since Magical Record adds a number of useful class methods to the NSManagedObject class, which will make working with the Note entity much easier as you will see in a few minutes. Select the Note entity in your Core Data model, create a new file, select the Core Data tab on the left, and choose the NSManagedObject subclass option on the right (figure 6).

Magical Record and Core Data: Create a Managed Object Subclass - Figure 6

Step 4: Create the Core Data Stack

Setting up a Core Data stack is quite a bit of work if you don't use one of the provided Xcode templates. With Magical Record, however, this is not the case. Head over to the application:didFinishLaunchingWithOptions: method of your application delegate and add the following code snippet.

[MagicalRecord setupCoreDataStack];

That's all there is to it. By default, the name of the store that Magical Record creates for you is identical to your application's name. However, you can customize the store's name by invoking setupCoreDataStackWithStoreNamed: instead and passing the name of the store.

Behind the scenes, Magical Record will instantiate a managed object context on the main thread as well as a persistent store coordinator and a managed object model. How magical is that?

Logging: The ability to log Core Data messages and errors to the console is built into Magical Record. Take a look at the console after building and running your application for the first time. The logs in the console show you exactly what Magical Record is doing behind the scenes.


Step 5: Laying the Foundation

Before we can start creating new notes, we first need to some grunt work. Revisit your Application Delegate's application:didFinishLaunchingWithOptions: method and initialize a navigation controller with the main view controller as its root view controller. Take a look at the complete implementation of application:didFinishLaunchingWithOptions:.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Setup Core Data Stack
    [MagicalRecord setupCoreDataStack];

    // Initialize View Controller
    self.viewController = [[MTViewController alloc] initWithNibName:@"MTViewController" bundle:nil];

    // Initialize Navigation Controller
    UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:self.viewController];

    // Initialize Window
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    [self.window setRootViewController:nc];
    [self.window makeKeyAndVisible];

    return YES;
}

We will display the notes in a table view so start by adding an outlet for a table view in the main view controller's header file. Select the main view controller's XIB file and drag a UITableView instance in the view controller's view. Don't forget to assign the File's Owner as the table view's dataSource and delegate. Also, make sure to connect the File's Owner table view outlet with the table view we just added to its view.

#import <UIKit/UIKit.h>

@interface MTViewController : UIViewController

@property (nonatomic, weak) IBOutlet UITableView *tableView;

@end

In the main view controller's implementation file, add a private property named notes to the class extension at the top. Make sure the property is of type NSMutableArray. The notes property will store the notes that we fetch from the data store and will serve as the table view's data source.

#import "MTViewController.h"

@interface MTViewController ()

@property (nonatomic, strong) NSMutableArray *notes;

@end

In the view controller's viewDidLoad method, we set the view up by calling setupView on the main view controller. This is nothing more than a helper method to keep the viewDidLoad method concise and uncluttered. In setupView, we add an edit and add button to the navigation bar and we fetch the notes from the data store by invoking the fetchNotes method.

- (void)viewDidLoad {
    [super viewDidLoad];

    // Setup View
    [self setupView];
}
- (void)setupView {
    // Create Edit Button
    UIBarButtonItem *editButton = [[UIBarButtonItem alloc] initWithTitle:@"Edit" style:UIBarButtonItemStyleBordered target:self action:@selector(editNotes:)];
    self.navigationItem.leftBarButtonItem = editButton;

    // Create Add Button
    UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithTitle:@"Add" style:UIBarButtonItemStyleBordered target:self action:@selector(addNote:)];
    self.navigationItem.rightBarButtonItem = addButton;

    // Fetch Notes
    [self fetchNotes];
}

It might surprise you that the fetchNotes method is only one line of code. This is all thanks to Magical Record. Fetching the notes from the data store is as simple as calling findAll on the Note class. The method returns an array of records as you'd expect. Don't forget the import the header file of the Note class at the top of the main view controller's implementation file.

- (void)fetchNotes {
    // Fetch Notes
    self.notes = [NSMutableArray arrayWithArray:[Note findAll]];
}

Sorting records is easy and elegant. Forget sort descriptors and take a look at the updated implementation of the fetchNotes method below.

- (void)fetchNotes {
    // Fetch Notes
    self.notes = [NSMutableArray arrayWithArray:[Note findAllSortedBy:@"date" ascending:YES]];
}

The editNotes: method is straightforward. All we do is toggle the editing style of the table view. That should be sufficient for now.

- (void)editNotes:(id)sender {
    [self.tableView setEditing:![self.tableView isEditing] animated:YES];
}

The addNote: method remains blank for the time being.

- (void)addNote:(id)sender {

}

Before building and running your application, we need to implement the required methods of the table view data source protocol. If you are familiar with iOS development, this shouldn't be too difficult. Take a look at the implementations of the various methods below for clarification.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.notes count];
}
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];

        // Configure Cell
        [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
    }

    // Fetch Note
    Note *note = [self.notes objectAtIndex:[indexPath row]];

    // Configure Cell
    [cell.textLabel setText:[note title]];
    [cell.detailTextLabel setText:[note keywords]];

    return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {

    }
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

Build and run your application.


Step 6: Creating Notes

When the user taps the add button, a modal view should appear allowing the user to add a new note to the data store. Create a new UIViewController subclass and name it MTEditNoteViewController. As the name indicates, we will also use this view controller to edit notes.

Magical Record and Core Data: Create the Edit Note View Controller - Figure 7

Before heading over to the view controller's XIB file, add three outlets to the view controller's header file. The first two outlets are instances of UITextField for the title and the keywords of the note. The third outlet is an instance of UITextView for the body of the note.

#import <UIKit/UIKit.h>

@interface MTEditNoteViewController : UIViewController

@property (nonatomic, weak) IBOutlet UITextField *titleField;
@property (nonatomic, weak) IBOutlet UITextField *keywordsField;
@property (nonatomic, weak) IBOutlet UITextView *bodyView;

@end

Creating the user interface of the edit note view controller shouldn't be too much of a challenge. Add two UITextField instances and one UITextView instance to the view controller's view. Configure the text fields as you wish, for example, by giving them a placeholder text. Don't forget to connect the File's Owner's outlets with the text fields and the text view.

Magical Record and Core Data: Create the User Interface of the Edit Note View Controller - Figure 8

Since we will be using the MTEditNoteViewController class for both adding and editing notes, it is important that we know what state (adding or editing) the view controller is in at any time. There are several ways to solve this problem. One way is to add a private note property to the view controller, which is nil if a new note is created and which is set during initialization when a note is being edited. In situations like this, I prefer to work with specialized initializers to avoid confusion and this is also what allows me to keep the note property private. In addition to the private note property, we also add a second private property named isEditing, a boolean. The reason for this will become clear in a few minutes. Also, don't forget to import the header file of the Note class.

#import "MTEditNoteViewController.h"

#import "Note.h"

@interface MTEditNoteViewController ()

@property (nonatomic, strong) Note *note;
@property (nonatomic, assign) BOOL isEditing;

@end

Let's go through the various methods step by step. First, we want to make sure that we can add new notes to the data store without problems. We start with the initWithNibName:bundle: method. The only change we make is setting the isEditing property to NO.

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];

    if (self) {
        // Set Flag
        self.isEditing = NO;
    }

    return self;
}

As we saw earlier, in the viewDidLoad method, we set the view up by invoking a setupView method in which we create the cancel and save buttons. Note that we only create the cancel button if the note property is equal to nil. The reason is that we present the view controller modally when adding new notes, but we push the view controller onto a navigation stack when we edit a note. If the note property is not equal to nil, we also populate the text fields and the text view with the contents of the note property.

- (void)viewDidLoad {
    [super viewDidLoad];

    // Setup View
    [self setupView];
}
- (void)setupView {
    // Create Cancel Button
    if (!self.note) {
        UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStyleBordered target:self action:@selector(cancel:)];
        self.navigationItem.leftBarButtonItem = cancelButton;
    }

    // Create Save Button
    UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleBordered target:self action:@selector(save:)];
    self.navigationItem.rightBarButtonItem = saveButton;

    if (self.note) {
        // Populate Form Fields
        [self.titleField setText:[self.note title]];
        [self.keywordsField setText:[self.note keywords]];
        [self.bodyView setText:[self.note body]];
    }
}

The cancel: method shouldn't hold any surprises.

- (void)cancel:(id)sender {
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

The save: method is a bit more verbose, but shouldn't be too difficult either. We first check whether the view controller's note property is set. If it is set then we know that a note is being edited, not created. If the note property is equal to nil then we know that a new note should be created. Dismissing the view controller is a bit tricky since we need to dismiss the view controller if it is presented modally when a note was created and pop it from the navigation stack when a note was edited. That is the reason we created the isEditing property.

- (void)save:(id)sender {
    if (!self.note) {
        // Create Note
        self.note = [Note createEntity];

        // Configure Note
        [self.note setDate:[NSDate date]];
    }

    // Configure Note
    [self.note setTitle:[self.titleField text]];
    [self.note setKeywords:[self.keywordsField text]];
    [self.note setBody:[self.bodyView text]];

    // Save Managed Object Context
    [[NSManagedObjectContext defaultContext] saveNestedContexts];

    if (self.isEditing) {
        // Pop View Controller from Navigation Stack
        [self.navigationController popViewControllerAnimated:YES];

    } else {
        // Dismiss View Controller
        [self dismissViewControllerAnimated:YES completion:nil];
    }
}

As you can see, creating a new note is another one-liner when using Magical Record. We populate the note with the contents of the form fields and save the managed object context of the note. Retrieving a reference to the managed object context is easy with Magical Record. All we need to do is ask the NSManagedObjectContext class for the default context. Saving the context is identical to saving a context without Magical Record. Even though we log the error if something goes wrong, this isn't really necessary since Magical Record will log any errors to the console for us.

It is now time to amend the addNote: method in the main view controller. Don't forget to import the header file of the MTEditNoteViewController class.

- (void)addNote:(id)sender {
    // Initialize Edit Note View Controller
    MTEditNoteViewController *vc = [[MTEditNoteViewController alloc] initWithNibName:@"MTEditNoteViewController" bundle:[NSBundle mainBundle]];

    // Initialize Navigation Controller
    UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:vc];

    // Present View Controller
    [self.navigationController presentViewController:nc animated:YES completion:nil];
}

Whenever a new note is added to the data store, we should update the table view to display the changes. For a production application, a better approach would be to observe changes in the managed object context. However, in this sample application, we fetch the notes from the data store and reload the table view every time the main view (re)appears. This is expensive and therefore not recommended if you plan to submit your application to the App Store. For situations like this, a fetched results controller is a perfect solution.

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    // Fetch Notes
    [self fetchNotes];

    // Reload Table View
    [self.tableView reloadData];
}

Step 7: Updating Notes

Updating notes is almost as easy as adding notes. As I mentioned earlier, we will create a specialized initializer to set the view controller's note property. Update the header file of the MTEditNoteViewController class by adding the new initializer as shown below. Don't forget to also add a forward class declaration for the Note class to the header file.

#import <UIKit/UIKit.h>

@class Note;

@interface MTEditNoteViewController : UIViewController

@property (nonatomic, weak) IBOutlet UITextField *titleField;
@property (nonatomic, weak) IBOutlet UITextField *keywordsField;
@property (nonatomic, weak) IBOutlet UITextView *bodyView;

- (id)initWithNote:(Note *)note;

@end

The specialized initializer isn't special actually. All we do is set the view controller's note and isEditing properties as you can see below.

- (id)initWithNote:(Note *)note {
    self = [self initWithNibName:@"MTEditNoteViewController" bundle:[NSBundle mainBundle]];

    if (self) {
        // Set Note
        self.note = note;

        // Set Flag
        self.isEditing = YES;
    }

    return self;
}

Before we build and run the application one more time, we need to update the main view controller's tableView:didSelectRowAtIndexPath: method. In this method, we fetch the correct note, initialize an instance of the MTEditNoteViewController class, and push the view controller onto the navigation stack.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    // Fetch Note
    Note *note = [self.notes objectAtIndex:[indexPath row]];

    // Initialize Edit Note View Controller
    MTEditNoteViewController *vc = [[MTEditNoteViewController alloc] initWithNote:note];

    // Push View Controller onto Navigation Stack
    [self.navigationController pushViewController:vc animated:YES];
}

Step 8: Deleting Notes

To delete note, we need to amend the tableView:commitEditingStyle:forRowAtIndexPath: method. We fetch the note, delete it from the data source and the managed object context, and update the table view. As you can see, deleting a record or entity from the data store is as simple as sending it a message of deleteEntity.

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Fetch Note
        Note *note = [self.notes objectAtIndex:[indexPath row]];

        // Delete Note from Data Source
        [self.notes removeObjectAtIndex:[indexPath row]];

        // Delete Entity
        [note deleteEntity];

        // Update Table View
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
    }
}

Scratching the Surface

By building Magical Record, we have only scratched the surface. I want to stress that Magical Record is a robust, mature library and not just an amalgamation of a handful of useful categories. As I showed you, Magical Record makes working with Core Data much easier and less verbose. Common tasks are often one-liners. Compare the following code snippets to fetch all the notes and sort them by date. Using Core Data this would result in the following code snippet.

NSFetchRequest *fr = [[NSFetchRequest alloc] init];
NSEntityDescription *ed = [NSEntityDescription entityForName:@"Note" inManagedObjectContext:[NSManagedObjectContext defaultContext]];
[fr setEntity:ed];

NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey:@"date" ascending:YES];
[fr setSortDescriptors:@[sd]];

NSError *error = nil;
NSArray *result = [[NSManagedObjectContext defaultContext] executeFetchRequest:fr error:&error];

Using Magical Record, however, this only requires one line of code.

NSArray *result = [Note findAllSortedBy:@"date" ascending:YES];

If we were to add the ability to search the list of notes, one approach - although not ideal - would be to search the data store for all the notes with a title or keyword that contained the query. Using Magical Record this would result in the following implementation.

NSPredicate *predicate1 = [NSPredicate predicateWithFormat:@"title contains[cd] %@", query];
NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"keywords contains[cd] %@", query];
NSPredicate *predicate = [NSCompoundPredicate orPredicateWithSubpredicates:@[predicate1, predicate2]];
NSArray *result = [Note findAllWithPredicate:predicate];

Conclusion

As I said, Magical Record has a lot more to offer than what I have showed you in this tutorial. Since version 2.0, Magical Record can deal with nested contexts and it also provides support for iCloud and threaded operations. The main goal of this tutorial is to show you that Core Data doesn't have to be cumbersome and Saul Mora illustrates this with Magical Record.

Advertisement