MagicalRecord Tutorial for iOS

A MagicalRecord tutorial for iOS that introduces the basics of setting up a CoreData stack and working with this popular library. By Andy Pereira.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Brewing a Beer (entity)

Now that your model and Core Data stack are set up, you can start adding beers to your list. Open BeerViewController.h, and after @class AMRatingControl, add:

@class Beer;

Also, add a Beer property after @interface:

@property (nonatomic, strong) Beer *beer;

Now switch to BeerViewController.m, and import Beer.h and BeerDetails.h at the top:

#import "Beer.h"
#import "BeerDetails.h"
    

In viewDidLoad, add the following block of code:

- (void)viewDidLoad {
    // 1. If there is no beer, create new Beer
    if (!self.beer) {
        self.beer = [Beer createEntity];
    }
    // 2. If there are no beer details, create new BeerDetails
    if (!self.beer.beerDetails) {
        self.beer.beerDetails = [BeerDetails createEntity];
    }
    // View setup
    // 3. Set the title, name, note field and rating of the beer
    self.title = self.beer.name ? self.beer.name : @"New Beer";
    self.beerNameField.text = self.beer.name;
    self.beerNotesView.text = self.beer.beerDetails.note;
    self.ratingControl.rating = [self.beer.beerDetails.rating integerValue];
    [self.cellOne addSubview:self.ratingControl];
	
    // 4. If there is an image path in the details, show it.
    if ([self.beer.beerDetails.image length] > 0) {
        // Image setup
        NSData *imgData = [NSData dataWithContentsOfFile:[NSHomeDirectory() stringByAppendingPathComponent:self.beer.beerDetails.image]];
         [self setImageForBeer:[UIImage imageWithData:imgData]];
     }
}
    

Troubleshooting: If you have errors after copying the preceding code into your project, clean your project by pressing Shift+Command+K, or by going to Product\Clean.

When BeerViewController loads, it will be because you have:

  • Either selected a beer, or…
  • Selected the Add button from the MasterViewController

Now, when the view loads you will want to do the following:

  1. Check if a beer object loaded. If not, this means you are adding a new Beer.
  2. If the beer doesn’t have any BeerDetails, create a BeerDetails object.
  3. To setup the view, grab the beer’s name, rating and note content. If there is no name for the beer (in the instance of a new beer, it will give it the name “New Beer”).
  4. If the beer contains a path to an image, it will load the image into the UIImageView.

There are a few more things you need to set up so that you can edit and add new beers. First, you need to be able to add or edit the name. Edit textFieldDidEndEditing: so it appears as below:

- (void)textFieldDidEndEditing:(UITextField *)textField {
    if ([textField.text length] > 0) {
        self.title = textField.text;
        self.beer.name = textField.text;
    }
}

Now, when you finish adding the name textField, it will set the beer’s name to whatever the contents of the textField are, so long the field is not empty.

To save the note content to the beer’s note value, find textViewDidEndEditing:, and edit it so it appears as below:

- (void)textViewDidEndEditing:(UITextView *)textView {
    [textView resignFirstResponder];
    if ([textView.text length] > 0) {
        self.beer.beerDetails.note = textView.text;
    }
}

Next, make sure the rating for the beer updates when the user changes it in the View Controller. Find updateRating, and add the following:

- (void)updateRating {
    self.beer.beerDetails.rating = @(self.ratingControl.rating);
}

When the user taps the UIImageView on the details page, it allows them to add or edit a photo. A UIActionSheet displays, which allows the user to pick an image from their Camera Roll, or to snap a new picture. If the user wants to take a picture, you’ll need to make sure the image also saves to the disk. Instead of saving the image to Core Data (which can cause performance issues) you’ll want to save the image to the user’s documents directory, with all their other pictures, and just use the image path for Core Data.

Manage interaction between the camera and the photo library by implementing methods of the UIImagePickerControllerDelegate protocol. You need to make the BeerViewController the delegate for the UIImagePickerController, since it’s the controller handling this storyboard scene. Find imagePickerController:didFinishPickingMediaWithinfo:, and add the following:

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
    // 1. Grab image and save to disk
    UIImage *image = info[UIImagePickerControllerOriginalImage];	
    // 2. Remove old image if present
    if (self.beer.beerDetails.image) {
        [ImageSaver deleteImageAtPath:self.beer.beerDetails.image];
    }
    // 3. Save the image
    if ([ImageSaver saveImageToDisk:image andToBeer:self.beer]) {
        [self setImageForBeer:image];
    }
    [picker dismissViewControllerAnimated:YES completion:nil];
}

Here’s what’s happening in the code shown above:

  1. imagePickerController:didFinishPickingMediaWithInfo: indirectly passes a reference to the user’s image of choice, where the image itself is in the info dictionary, under the UIImagePickerControllerOriginalImage key
  2. If the Beer object already contains an image, the app will delete it from the disk, so you don’t fill up the user’s storage unnecessarily.
  3. The new image then saves to the disk, and the path is added to the BeerDetails image attribute. Open ImageSaver and find saveImageToDisk:andToBeer to see how this looks. Once the image successfully saves, it shows in the UIImageView.
  4. The picker is dismissed.

You’ll need to modify ImageSaver just a little to get it to work. Now that you’ve got your Beer classes created, you can uncomment the import lines, and the line that sets the image’s path in the entity. Open ImageSaver.m and modify the import statements to this:

#import "ImageSaver.h"
#import "Beer.h"
#import "BeerDetails.h"

Now you’ll need to uncomment the line found within the IF statement:

if ([imgData writeToFile:jpgPath atomically:YES]) {
        beer.beerDetails.image = path;
}

The ImageSaver class is now fully ready to accept an image, and save it to the phone’s documents directory, and the path to the BeerDetails object.

From here, the user has two options: Cancel or Done, which saves the new beer. When you created the view, a Beer entity was created, and inserted in the managedObjectContext. Canceling should delete the Beer object. Find cancelAdd, and add the following:

- (void)cancelAdd {
    [self.beer deleteEntity];
    [self.navigationController popViewControllerAnimated:YES];
}

MagicalRecord provides a nice method for deleting an entity, which automatically removes the entity from the managedObjectContext. After deleting, the user will return to the main list of beers.

If the user chooses Done, it will save the beer and return to the main list. Find addNewBeer. It simply pops the view controller, going back to the list. When the view disappears, it will call viewWillDisapper:. This in turn calls saveContext.

Right now saveContext is empty, so you’ll need to add some code to save your entity. Add the following to saveContext:

- (void)saveContext {
    [[NSManagedObjectContext defaultContext] saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
        if (success) {
            NSLog(@"You successfully saved your context.");
        } else if (error) {
            NSLog(@"Error saving context: %@", error.description);
        }
    }];
}

There’s a lot going on here in just a few lines of code! In AppDelegate.m, you set up the Core Data stack with MagicalRecord. This created a default managedObjectContext that the entire app can access. When you created the Beer and BeerDetails entities, they were inserted into this defaultContext . MagicalRecord allows you to save any managedObjectContext you may have using saveToPersistentStoreWithCompletion:. The completion block gives you access to an NSError object, if the save failed. Here, you’ve added a simple if/else block that logs what happens after you try to save the defaultContext.

Are you ready to test it out? Go ahead and run your app! Select the + button, fill out any information you want. Then select done.
Working Details Page

When you do, you’ll notice you don’t see your new beer show up in the list. Don’t fret, it’s not broken and you’ll learn how to fix this in just a bit. You also might notice your debugger has a whole bunch of information in it. Contrary to what your logic might say, this is really good for you!
Debugger

Contributors

Over 300 content creators. Join our team.