How to Use Cocoa Bindings and Core Data in a Mac App

This is a blog post by Andy Pereira, a software developer at USAA in San Antonio, TX, and freelance iOS and OS X developer. Lately we’re starting to write more Mac app development tutorials on raywenderlich.com, since it’s a natural “next step” for iOS developers to learn! In our previous tutorial series by Ernesto Garcia, […] By Andy Pereira.

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

Pre Populating Bugs

Open AppDelegate.m and add the following method:

-(void)prePopulate {
    if (![[NSUserDefaults standardUserDefaults] valueForKey:@"sb_FirstRun"]) {
        NSString *file = @"file://";
        NSManagedObject *centipede = [[NSManagedObject alloc] initWithEntity:[NSEntityDescription entityForName:@"Bug" inManagedObjectContext:self.managedObjectContext] insertIntoManagedObjectContext:self.managedObjectContext];
        [centipede setValue:[NSNumber numberWithFloat:3] forKey:@"rating"];
        [centipede setValue:@"Centipede" forKey:@"name"];
        [centipede setValue:[file stringByAppendingString:[[NSBundle mainBundle] pathForImageResource:@"centipede.jpg"]] forKey:@"imagePath"];
        
        NSManagedObject *potatoBug = [[NSManagedObject alloc] initWithEntity:[NSEntityDescription entityForName:@"Bug" inManagedObjectContext:self.managedObjectContext] insertIntoManagedObjectContext:self.managedObjectContext];
        [potatoBug setValue:[NSNumber numberWithFloat:4] forKey:@"rating"];
        [potatoBug setValue:@"Potato Bug" forKey:@"name"];
        [potatoBug setValue:[file stringByAppendingString:[[NSBundle mainBundle] pathForImageResource:@"potatoBug.jpg"]] forKey:@"imagePath"];
        
        NSManagedObject *wolfSpider = [[NSManagedObject alloc] initWithEntity:[NSEntityDescription entityForName:@"Bug" inManagedObjectContext:self.managedObjectContext] insertIntoManagedObjectContext:self.managedObjectContext];
        [wolfSpider setValue:[NSNumber numberWithFloat:5] forKey:@"rating"];
        [wolfSpider setValue:@"Wolf Spider" forKey:@"name"];
        [wolfSpider setValue:[file stringByAppendingString:[[NSBundle mainBundle] pathForImageResource:@"wolfSpider.jpg"]] forKey:@"imagePath"];
        
        NSManagedObject *ladyBug = [[NSManagedObject alloc] initWithEntity:[NSEntityDescription entityForName:@"Bug" inManagedObjectContext:self.managedObjectContext] insertIntoManagedObjectContext:self.managedObjectContext];
        [ladyBug setValue:[NSNumber numberWithFloat:1] forKey:@"rating"];
        [ladyBug setValue:@"Lady Bug" forKey:@"name"];
        [ladyBug setValue:[file stringByAppendingString:[[NSBundle mainBundle] pathForImageResource:@"ladybug.jpg"]] forKey:@"imagePath"];
        [[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:YES] forKey:@"sb_FirstRun"];
    }
}

In the code above, you’ve created a way to use an NSUserDefault value to determine if the app has been run before. If the app doesn’t have a value for sb_FirstRun, it will create 4 new Bug entities and set sb_FirstRun, so that the same initial Bug information is not added to the app multiple times.

Note: NSUserDefaults allows you to create key-value user settings. You could store just about anything you’d like using NSUserDefaults, but you should try to limit it to user settings.

At the end of applicationDidFinishLaunching:, add the following line to call the new method:

[self prePopulate];

Run the app, and you should see the 4 original tutorial bugs, with their names, images, and ratings, just like in the screenshot below:

Note:If you had previously added some data to the app, that data will remain intact. If you had added a Lady Bug record as mentioned previously, you’ll notice that you now have two Lady Bug records, since the initial data addition routine does not check for duplicates!

Finishing Touches

When working with Core Data, your managedObjectContext isn’t saved until you specifically instruct it to. This is why your bug records aren’t saved unless you quit the app by using the Quit menu option. Check applicationShouldTerminate: in AppDelegate.m to see the relevant code.

If your app crashes, or you stop the app via Xcode rather than quitting, you will likely lose any unsaved data. You should provide the user with a way to manually save their data at any point, or else you’ll drive your users buggy! :]

Go to MainMenu.xib. In interface builder, you should see a menu bar. If not, you can select Main Menu from the outline view. There are many menu items that you will not need, so remove Edit, Format, and View from the menu by selecting them and clicking delete.

If you select the menu items on the main Interface Builder, view and delete them, you’ll notice that there’s a gap left behind! This is because the full menu item sometimes doesn’t get deleted properly.

If this happens to you, use the Document Outline view and remove the relevant menu items.

Your resulting menu should look like:

Select the File menu item in IB, and then Control+drag from Save to App Delegate in the Document Outline. Select saveAction: from the poup. Now, whenever a user performs a File\Save, the context will be saved.

In the File menu, change the title for Revert to Saved to Revert to Original via the Attributes Inspector. Then, select the Key Equivalent field and press ⌘R. This will set the menu item’s shortcut to ⌘R. This menu item will delete all of the current Bug records and replace them with the original set, as below:

In AppDelegate.h add the following method definition:

-(IBAction)resetBugs:(id)sender;

Switch to AppDelegate.m and add the following import:

#import "Bug.h"

Next add the following method:

-(IBAction)resetBugs:(id)sender {
    NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:@"Bug"];
    NSError *error;
    NSArray *allBugs = [self.managedObjectContext executeFetchRequest:request error:&error];
    for (Bug *bug in allBugs) {
        [self.managedObjectContext deleteObject:bug];
    }
    if (!error) {
        [self saveAction:self];
        [[NSUserDefaults standardUserDefaults] setValue:nil forKey:@"sb_FirstRun"];
        [self prePopulate];
    }
}

The code above will do an NSFetchRequest to get all of the bugs and delete them. It will then save the context and set sb_FirstRun to nil so that when prePopulate gets called, it will create all the default bugs.

Go to MainMenu.xib and Control+drag from Revert to Original to App Delegate and select resetBugs:, as below:

Build and run the app!

Remove a couple of bug records. Then select File\Revert to Original. All of the original bugs should reappear. You can also try making some changes to a bug record, using File\Save, and then stopping the app via Xcode. Your changes should still be intact when you next run the app.

Subclassing NSArrayController

Currently, when you delete a bug record, there is no confirmation at all. You tap the minus (-) button and the record immediately gets deleted. But what if you accidentally tapped the button? There is no way to get the record back. It’s best to add a confirmation before you do a destructive operation! :]

However, there is no direct way to control the deletion of NSManagedObjects in the application in it’s present state. One way to handle the deletion of objects is through subclassing NSArrayController and overriding the methods used to remove an object.

Go to File\New File, select the Objective-C Class template, name the class BugArrayController, and make it a subclass of NSArrayController, as below:

Open BugArrayController.h and add support for the NSAlertDelegate protocol by changing the @interface line to look like:

#import <Cocoa/Cocoa.h>

@interface BugArrayController : NSArrayController <NSAlertDelegate>

@end

Switch to BugArrayController.m and add the following import at the top:

#import "Bug.h"

Next, add the following methods:

-(void)remove:(id)sender {
    NSAlert *alert = [[NSAlert alloc] init];
    [alert addButtonWithTitle:@"Delete"];
    [alert addButtonWithTitle:@"Cancel"];
    [alert setMessageText:@"Do you really want to delete this scary bug?"];
    [alert setInformativeText:@"Deleting a scary bug cannot be undone."];
    [alert setAlertStyle:NSWarningAlertStyle];
    [alert setDelegate:self];
    [alert respondsToSelector:@selector(doRemove)];
    [alert beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow] modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:nil];
}

-(void)alertDidEnd:(NSAlert*)alert returnCode:(NSInteger)returnCode contextInfo:(void*)contextInfo {
    if (returnCode ==  NSAlertFirstButtonReturn) {
        // We want to remove the saved image along with the bug
        Bug *bug = [[self selectedObjects] objectAtIndex:0];
        NSString *name = [bug valueForKey:@"name"];
        if (!([name isEqualToString:@"Centipede"] || [name isEqualToString:@"Potato Bug"] || [name isEqualToString:@"Wolf Spider"] || [name isEqualToString:@"Lady Bug"])) {
            NSError *error = nil;
            NSFileManager *manager = [[NSFileManager alloc] init];
            [manager removeItemAtURL:[NSURL URLWithString:bug.imagePath] error:&error];

        }
        [super remove:self];
    }
}

By overriding the remove method, you prevent the deletion from happening immediately. Instead, you create an NSAlert to warn the user that what is being done cannot be undone. When the user taps a button on the alert dialog, the alertDidEnd:returnCode:contextInfo: delegate method is executed.

alertDidEnd:returnCode:contextInfo: checks to see if the user elected to continue with the deletion by checking if the Delete button was tapped. If so, delete the first selected object in the NSAraryController.

An additional bonus of overriding NSArrayController is that you can now delete the image for the deleted bug record from the Application Support directory. In the original version of the code, this would not have been possible. There is also a check to make sure that none of the images for the original data are deleted, since those images come directly from the application bundle.

Now it’s time to use your new BugArrayController class, instead of using a plain old NSArrayController!]

Open MasterViewController.h and add the following import:

#import "BugArrayController.h"

Then, change:

@property (strong) IBOutlet NSArrayController *bugArrayController;

to:

@property (strong) IBOutlet BugArrayController *bugArrayController;

Next, open MasterViewController.xib, select BugArrayController in the Document Outline, and change its Class to BugArrayController in the Identity Inspector, as such:

Sometimes Xcode doesn’t recognize that you’ve changed the class for the BugArrayController. In this case, right-click on the “-” button, and remove the action remove:. Then Control+drag from the “-” button to BugArrayController, and select remove: again to associate the button with the method on the new class.

Save your changes and build and run the app!

Click the minus (-) button for any record — you should now get a warning. If you click Delete, your bug will be removed, but clicking Cancel aborts the deletion process:

And you’re done! Because you know that the only thing more scary than bugs is deleting your hard-entered data by mistake ;]

Contributors

Over 300 content creators. Join our team.