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.
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.
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).
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).
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.
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.
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 NSError *error = nil; [[NSManagedObjectContext defaultContext] save:&error]; if (error) { NSLog(@"Unable to save new note to data store."); } 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.