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

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

Learn how to use Cocoa Bindings in your Mac apps!

Learn how to use Cocoa Bindings in your Mac apps!

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, you learned how to create a very simple Mac app called “Scary Bugs” with a table view, images, and editing capabilities.

In this tutorial you’ll go a bit deeper, and will learn how to:

  • Use Core Data to save your Scary Bugs
  • Utilize an NSArrayController
  • Implement Cocoa Bindings
  • Transform values using NSValueTransformers

Before you get started on this tutorial, you’ll need to be familiar with the information in the tutorials below:

Okay — ready to dive into the depths of Mac development? Great! Time to get started! :]

An Introduction to Cocoa Bindings

Apple describes Cocoa Bindings as: “a collection of technologies that help you encapsulate data, and write less glue code”. “Glue code” is the code in your app that doesn’t actually perform any real function, but helps you out in sticky situations (pun fully intended) when you are trying to tie together sets of code that weren’t designed to interoperate.

For example, in the Scary Bugs app you’ve been working on, your goal has been to display the ScaryBugData’s title and rating properties in the text field and rating view in the right side of the window:

You don't need to write glue code to do this on the Mac, thanks to Cocoa Bindings!

In order to do this, so far you have written some “glue code” like this:

-(void)setDetailInfo:(ScaryBugDoc*)doc 
{
    NSString    *title = @"";
    NSImage     *image = nil;
    float rating=0.0;
    if( doc != nil )
    {
        title = doc.data.title;
        image = doc.fullImage;
        rating = doc.data.rating;
    }
    [self.bugTitleView setStringValue:title];
    [self.bugImageView setImage:image];
    [self.bugRating setRating:rating];
 
}

You’re probably used to doing this all the time as an iOS developer, but wouldn’t it be nice if you didn’t have to write that code at all? That’s exactly what Cocoa Bindings does for you!

Basically, Cocoa Bindings allows you to use the properties inspector to bind a UI control to a property on an object. Then the control will display the value of the property, and when you modify the value of the control it will update the value of the property.

A screenshot of Cocoa Bindings

This tutorial will give you hands on experience with how this works – you will convert the ScaryBugs project to use Cocoa Bindings and remove all need for the old “glue code!”

By the time you’re done, you’ll really start wishing iOS had this cool feature ;]

Behind the Scenes: KVO

You might wonder how this magic all works behind the scenes.

If you’ve ever used key-value coding and observing in iOS, you might have a good guess – and you’d be right!

Assume you have a class named “Person”, with an NSNumber property named “age”. One way of setting the property would be as follows:

[aPerson setAge:@26];

An alternative way of assigning the property would be:

aPerson.age = @26;

The key-value coding way of doing it would be like this:

[aPerson setValue:@26 forKey:@"age"];

In short, key-value coding allows you to set the value of a property by the name, or the key, of the property. Behind the scenes, Cocoa Bindings uses KVO to update the associated property when a UI control is edited.

In addition to key-value coding, Cocoa Bindings also uses key-value observing (KVO). KVO allows you to register observers for an object’s properties. The observer implements key-value observing as follows:

observeValueForKeyPath:ofObject:change:context:

So whenever a change to the observed property occurs, observeValueForKeyPath is called and passed several parameters, including the object value and the property key.

Cocoa Bindings uses this as well behind the scenes, so it knows to update the controls when the property changes.

If you are interested in learning more about how Cocoa Bindings works behind the scenes, check out these documents:

Getting Started

In this tutorial, you will continue on with the project from the previous tutorial series. However, there have been a few changes:

  • All of the methods and outlets that used to implement the “glue code” have been removed from the app, since you will not be needing those anymore!
  • This tutorial is going to use Core Data, so I’ve added a few Core Data related methods and properties into AppDelegate.m to make it easier to get started.
  • Converted the table view to be from view-based to cell-based, to keep the tutorial simpler, as I’ll explain later.

Go ahead and download the starter project and open it in Xcode. Build and run the app to make sure it runs – at this point you should just see a blank screen like this (and none of the buttons work):

Now get ready to start coding! :]

Bug Entity

Go to File/New/File…. Under OS X, click “Core Data”, and choose “Data Model”. Click “Next,” and name itScaryBugsApp.xcdatamodeld.

Note: it’s important that you use the suggested names in this tutorial, so that the existing project code — and the tutorial explanation of the code — line up! :]

Select ScaryBugsApp.xcdatamodeld. Click “Add Entity,” and name the Entity Bug.

You need to add three attributes to your Bug entity: namerating, and imagePath. Under Attributes, click the “+” three times, each time naming the attribute as “name”, “rating”, and “imagePath”.

Set the Type of each attribute so that name and imagePath are both “String,” and rating is “Float”.

Select “rating”, and then select the Data Model inspector (the third tab on the top half of the right sidebar) in the utilities drawer. Under the Attribute section, you will see a subsection named “Validation.” For Minimum, enter 1; Maximum enter 5; and set Default to 1. Doing this will guarantee that a rating will never be a value other than 1 – 5.

Save your changes with Command-S, then select Editor/Create NSManagedObject Subclass:

If you’re asked what entities you would like to manage in the step above, select “Bug” and click “Next”. Finally, click “Create”.

Go ahead, build and run your application to make sure that everything is OK so far. You should not receive any warnings or errors, but if you do verify the following:

  • Check the name of the .xcdatamodeld file.
  • Check that each attribute has a type associated with it.

Everything look good? Okay, move on to working with the NSArrayController! :]

NSArrayController

Now that you have your Entity working, it’s time to set up an NSArrayController. You’re likely wondering “what is an NSArrayController, and why do I need it?” :]

An NSArrayController is a bindings compatible class, adding features for sorting and selection management. When you populate a table with data from an array, you typically have to calculate which row a user has selected from the table, and grab the object from that index in the array. NSArrayController provides you with a method to return the object that is associated with whichever table row has been selected. This prevents you from having to do copious amounts of login in methods like selectionDidChange:, and sometimes even removes the need to have them at all. In this tutorial, you’ll actually get rid of selectionDidChange: entirely, giving you a chance to see how much work Bindings really will do for you.

An NSArrayController, as its name implies, controls an array, or collection, of objects. It can also be used to manage the relationships of an NSManagedObject, which is usually used to create Core Data models. There are also Object, Dictionary, Tree, and User Default Controllers that can be used with Bindings.

For instance, the Bug class that you created earlier is a sub-class of NSManagedObject and you’ll be using NSArrayController to work with the Bug class.

Open MasterViewController.h, and add the following property:

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

Now switch to AppDelegate.m, and add the following to the end of applicationDidFinishLaunching::

self.masterViewController.managedObjectContext = self.managedObjectContext;

This will allow you to use the same instance of the managedObjectContext that is created when the app is loaded, from the MasterViewController. This is going to be critical for setting up the NSArrayController.

Switch to MasterViewController.xib, and drag an Array Controller from the Object Library (third tab on the lower half of the right sidebar) to the objects list in the Document Outline:

Select the Array Controller, hit “Enter,” and rename the Array Controller to BugArrayController.

Note: it may appear that the name change did not take place when you enter the new name for the Array Controller. Just switch to some other file and then switch back to MasterViewController.xib. Now you should see that the array controller shows up with the correct name! :]

Select BugArrayController in the Document Outline, then select the Attributes Inspector in the right sidebar. Under Object Controller, change Mode to “Entity Name”; for Entity Name, enter “Bug”; and finally check the box “Prepares Content”, as shown in the following screenshot:

Entering the name Bug for entity name tells the BugArrayController that it is going to be managing Bug objects, while “Prepares Content” will make the BugArrayController grab any content from the managed object context.

Switch to the Bindings Inspector — the 7th tab on the right sidebar. Under “Parameters,” expand “Managed Object Context.” Check “Bind To,” and in the drop down, select “File’s Owner.” Change the Model Key Path from “self” to “managedObjectContext”, as shown below:

You have just set the BugArrayController to use the objects found in MasterViewController’s managedObjectContext. Again, this is another great example on how using an NSArrayController eliminates the need to write a bunch of boiler plate or glue code.

Go ahead — Build and Run your app! Your app should look the same as before, but make sure the Log is not giving you any warnings or errors.

You may not realize it yet, but you have just created something wonderful: your BugArrayController will now contain any and all Bug entities that are created in your managedObjectContext!

Bindings

While working in XCode is hardly comparable to dabbling in the dark arts, Cocoa Bindings isn’t too far off from appearing magical! :] Now you’re finally going to try out Bindings.

Switch to MasterViewController.xib, Control-drag from the plus (+) button to the BugArrayControllerand select the “add:” action from the popup menu, as below:

Do the same for the “-” button, but this time select “remove:”.

Now, whenever you click “+”, the NSArrayController will add a Bug entity to the managedObjectContext, and it will also remove one whenever you click “-”.

Wow, pretty easy, eh? Just think of all the glue code you didn’t have to write! :]

Next select the textfield under “Name” in the interface. With the Bindings Inspector selected, expand Valueunder the Value section. Check the Bind To checkbox, make sure the dropdown has BugArrayController selected, and enter name for the Model Key Path as demonstrated in this screenshot:

Remember how NSArrayControllers manage what objects are selected in a table? Whatever row is selected in the table a Bug object resides in an array within the NSArrayController called

selectedObjects

at index 0. When using the bindings view in Interface Builder, the “selection” key refers to this same object. Using “name” for model key path refers to the “name” property of the Bug object. The name field will now display the name value in whatever Bug has been selected in the table.

Next, time to set up the table view!

Note: In this tutorial, the NSTableView on the view has been changed to be cell-based. As this tutorial is only an introduction to Cocoa Bindings, it’s likely easier to grasp the tutorial concepts this way, as opposed to using a View-based NSTableView. Apple cautions that you should be extremely familiar with bindings and View-based table views before using them together.

Select the TextCell Column in the TableView. This step can be a bit tricky, as the first click on the table will select the ScrollView, the second click will select the tableView, the third click will select the column, and the fourth click will select the individual TextCell column.

However, you can instead choose to use the Document Outline to expand the table view structure and find the column that way. Switch to the Attributes inspector, and once the column is properly selected, the inspector will display information about the column like this:

Switch back to the Bindings Inspector, and expand Value. Check the Bind To box, make sure BugArrayController is selected in the drop down, and enter name for the Model Key Path, exactly as you did with the name textfield.

As with setting up the binding on the “name” text field, you are setting the table to display the value for “name” for all of the objects in the NSArrayController. However, using the “arrangedObjects” key returns all of the objects, which is exactly what the table needs, as you are wanting all of the Bug objects to be listed out.

Save your changes — time to Build and Run the application!

Click the + button — it should add an entry to the table view, and the Name textfield should be empty. Enter the name “Lady Bug” in the name field, and click Enter. You should now see the name Lady Bug in the table and in your name field:

Quit your app by selecting ScaryBugsApp/Quit(⌘+Q). Run the app in Xcode again, and you should still see Lady Bug in the table. If you did, you have successfully set up bindings! Congratulations! :]

Note: If you stop the app in Xcode, or close the app via the close button, instead of quitting the app as mentioned above, your changes will not be saved. So make sure to follow the steps as directed! :]

Bindings: Next Steps

Open the Assistant Editor by clicking the second button on the Editor group of buttons on the Xcode toolbar on the top right. Add an outlet by Control+dragging from BugArrayController to MasterViewController.h.

Name the outlet bugArrayController as shown below:

You can’t directly set a binding for the EDStarRating view’s value through Interface Builder, which is the view for setting a bug rating with the surprised faces. Because EDStarRating’s view is created programmatically, you’ll need to add a little bit of code to make that part work.

Go to MasterViewController.m. At the top, add an import as follows:

#import "Bug.h"

Next, add the following method anywhere in the file between the @implementation and @end lines):

-(Bug*)getCurrentBug {
    if ([[self.bugArrayController selectedObjects] count] > 0) {
        return [[self.bugArrayController selectedObjects] objectAtIndex:0];
    } else {
        return nil;
    }
}

The method above finds which item is selected in the table view (and in actual fact, finds what is selected in the NSArrayController), and returns that entity.

At the end of loadView, add the following :

-(void)loadView
{
    [super loadView];
    self.bugRating.starImage = [NSImage imageNamed:@"star.png"];
    self.bugRating.starHighlightedImage = [NSImage imageNamed:@"shockedface2_full.png"];
    self.bugRating.starImage = [NSImage imageNamed:@"shockedface2_empty.png"];
    self.bugRating.maxRating = 5.0;
    self.bugRating.delegate = (id<EDStarRatingProtocol>) self;
    self.bugRating.horizontalMargin = 12;
    self.bugRating.editable = NO;
    self.bugRating.displayMode = EDStarRatingDisplayFull;
    self.bugRating.rating = 0.0;
 
    // Manual Bindings
    [self.bugRating bind:@"rating" toObject:self.bugArrayController withKeyPath:@"selection.rating" options:nil];
    [self.bugRating bind:@"editable" toObject:self.bugArrayController withKeyPath:@"selection.@count" options:nil];
}

The last two lines you added are programmatically created bindings, rather than bindings created through Interface Builder. Because EDStarRating’s view is created programmatically, you’ll also need to set your bindings programmatically. Here, the bindings for the displayed rating, and whether or not the ratings view is editable are set.

There are four things you need to do in order to programmatically set a binding on an object:

  1. bind: Know the key you are binding. In this case, you’re binding both “rating” and “editable” respectively.
  2. toObject: Tell your object what object it’s getting bound to, which is your NSArrayController, bugArrayController.
  3. withKeyPath: The key-path refers to what property or value of your object is getting bound. For rating, you set “selection.rating,” just as you did with the name text field. “selection.@count” will be explained a little bit later, so watch out for that.
  4. options: Finally, you can choose to have “options.” In both cases here you’re passing nil because you don’t need to set values like default placeholder text, or “validates on update.” Just about all of the options you can set are available to you in Interface Builder’s bindings view.

Also replace starsSelectionChanged:rating: with the following:

-(void)starsSelectionChanged:(EDStarRating*)control rating:(float)rating {
    Bug *selectedBug = [self getCurrentBug];
    if (selectedBug) {
        selectedBug.rating = [NSNumber numberWithFloat:self.bugRating.rating];
    }
}

The method above handles the update of the currently selected Bug instance after the rating has been changed.

The above two methods are very similar to the equivalent methods from the previous tutorial, but they have been reworked to using Core Data.

Build and Run the app! You can now change the rating for the Lady Bug record, and for any new bug you may wish to add. If your table is empty, you can’t change the ratings view.

Bindings and Enabling

Since the ratings view is disabled if there’s no data to edit, you can now use bindings to disable the name field, “-” button and “Change Picture” button if the table is empty, just as you did in the previous tutorial.

Switch to MasterViewController.xib. Select the minus (-) button, and go the Bindings Inspector. Under Availability, expand Enabled. In the Model Key Path, enter @count and hit enter. Do the same for the name text field and the “Change Picture” button, as below:

Using the selection key here means you are wanting behavior the relates specifically to an object being selected in the table. The “@count” is a Collection Operator that returns the actual count of the selectedObjects array. If it is 0, then these elements will be disabled; if it is 1, they will enable.

Run the app and click the minus (-) button to delete records until the table is empty. You should see that the buttons and text field are now disabled. Adding a new entry re-enables them, as shown in the screenshot here:

NSValueTransformers: More than meets the eye

When you work with Core Data (or any database), you have several ways to save images. One way is to save the image directly to the database. Another option is to save the image to a location on disk, and then save the path to the image in the database as a string.

You’ll notice that when you made your Bug entity, the imagePath attribute had a type of String. This is because you are not going to save the image directly to Core Data, but instead will save it to the Application Support directory.

Saving an image directory to Core Data can be taxing. By saving the location of the image to Core Data, and the image to a safe location, you run lessen the chances of poor performance of your applications.

Are you wondering how a string is going to turn into an image using bindings? Unfortunately, this doesn’t happen automagically. But it can be done with only a few lines of code using an NSValueTransformer.

What’s an NSValueTransformer? It is exactly what it sounds like: a value transformer! :] It takes one value and changes, or transforms, it into something else.

You’re going to create two new classes which are value transformers – one to handle changing the path string to an image in the detail area, and another to handle changing the path to a thumbnail in the table view.

Create the first class by going to File\New\File…. Choose the OS X\Cocoa\Objective-C class template from the choices. Name the class DetailImageTransformer, and make it a subclass of NSValueTransformer.

Add the following code to DetailImageTransformer.m (between the @implmentation and @end lines):

+(Class)transformedValueClass {
    return [NSImage class];
}
 
-(id)transformedValue:(id)value {
    if (value == nil) {
        return nil;
    } else {
        return [[NSImage alloc] initWithContentsOfURL:[NSURL URLWithString:value]];
    }
}

In the code above you first create a class method that returns a Class, which in this case returns an NSImage class. NSValueTransformers can transform the value of any class to another, and

transformedValueClass

simply returns the class type that will come from

transformedValue:

.

The second method, transformedValue:, gets a parameter named value passed to it. This value is going to be the path string that is stored in the entity’s imagePath attribute. If the value is empty, then do nothing. However, if it has a value, then return an NSImage with the contents of the image at the specified path.

You might ask yourself why there isn’t a conversion going the other way, and what a great question that is. You can override

reverseTransformedValue:(id)value

and do exactly that. However, for this application, it isn’t necessary, as we are not saving the images through a drag-and-drop, or some other alternative scenario.

In the same fashion, create another class by going to File\New\File…. Choose the OS X\Cocoa\Objective-C class template from the choices. Name the class TableImageCellTransformer, and make it a subclass ofNSValueTransformer.

Open TableImageCellTransformer.m and add the following import to it at the top:

#import "NSImage+Extras.h"

Then, add the following code to the class implementation:

+(Class)transformedValueClass {
    return [NSImage class];
}
 
-(id)transformedValue:(id)value {
    if (value == nil) {
        return nil;
    } else {
        NSImage *image = [[NSImage alloc] initWithContentsOfURL:[NSURL URLWithString:value]];
        image = [image imageByScalingAndCroppingForSize:CGSizeMake( 44, 44 )];
        return image;
    }
}

The above code is very similar to what you did in DetailImageTransformer. The only difference is that when you transform the path, instead of simply returning an image, you scale the image down to 44 x 44 to create a thumbnail version, then return the thumbnail to the caller.

In MasterViewController.m, replace the empty implementation for changePicture: with the following:

-(IBAction)changePicture:(id)sender {
    Bug *selectedBug = [self getCurrentBug];
    if (selectedBug) {
        [[IKPictureTaker pictureTaker] beginPictureTakerSheetForWindow:self.view.window withDelegate:self didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:) contextInfo:nil];
    }
}

This code is very similar to the previous tutorial; the only difference is that the above method checks if a Bug Entity, rather than a SelectedBugDoc.

IKPictureTaker is a really helpful class which allows users to choose images by browsing the file system. However, it doesn’t return a name for the image it gets as it is not saving the path or name of the image, just an NSImage instance. To remedy this, you will create a unique string generator to provide a name for the selected images.

Add the following method to MasterViewController.m:

// Create a unique string for the images
-(NSString *)createUniqueString {
    CFUUIDRef theUUID = CFUUIDCreate(NULL);
    CFStringRef string = CFUUIDCreateString(NULL, theUUID);
    CFRelease(theUUID);
    return (__bridge NSString *)string;
}

createUniqueString() will return a string made from a UUID, thus ensuring that the photos you add to the application are never named the same as another.

Next, you need a way to actually save an image to your Application Support directory. This is important so that no matter what happens to the original image that was selected by the user, the application will still be able to display an image for each record in the application.

Switch to MasterViewController.h and add the following property:

@property (strong, nonatomic) NSURL *pathToAppSupport;

The above property will hold the path to the Application Support directory which is where you’ll be storing the images for the app.

Next, switch back to MasterViewController.m and add the following method:

-(BOOL)saveBugImage:(NSImage*)image toBug:(Bug*)bug {
    // 1. Get an NSBitmapImageRep from the image passed in
    [image lockFocus];
    NSBitmapImageRep *imgRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0.0, 0.0, [image size].width, [image size].height)];
    [image unlockFocus];
 
    // 2. Create URL to where image will be saved
    NSURL *pathToImage = [self.pathToAppSupport URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.png",[self createUniqueString]]];
    NSData *data = [imgRep representationUsingType: NSPNGFileType properties: nil];
 
    // 3. Write image to disk, set path in Bug
    if ([data writeToURL:pathToImage atomically:NO]) {
        bug.imagePath = [pathToImage absoluteString];
        return YES;
    } else {
        return NO;
    }
}

If you review the code above step-by-step, you’ll see that the following actions take place:

  1. Create an NSBitmapImageRep from the image passed in.
  2. Create a unique url string with the .png extension, and append the resulting string to the Application Support directory path. An NSData value is then created from the NSBitmapImageRep.
  3. The data is written to the Application Support directory using the path set in pathToImage. As well, the path string is saved to the current bug’s imagePath attribute.

Next switch back to MasterViewController.m and replace the existing empty implementation of pictureTakerDidEnd:returnCode:contextInfo: with the following:

-(void) pictureTakerDidEnd:(IKPictureTaker *) picker
                 returnCode:(NSInteger) code
                contextInfo:(void*) contextInfo {
    NSImage *image = [picker outputImage];
    if( image !=nil && (code == NSOKButton) )
    {
        if ([self makeOrFindAppSupportDirectory]) {
            Bug *bug = [self getCurrentBug];
            if (bug) {
                [self saveBugImage:image toBug:bug];
            }
        }
    }
}

The above code gets the image from the image picker. If the image is valid and the user did not cancel the picker, then the code calls a method to create or find the Application Support directory. This function doesn’t exist yet — you’ll create it a bit later in this tutorial.

If creating or finding the Application Support directory was successful, then the code gets the current Bug. Finally if there is a selected bug, then save the image path to that bug record.

Now add the makeOrFindAppSupportDirectory method referenced above which guarantees that there will be a directory to save the image to:

-(BOOL)makeOrFindAppSupportDirectory {
    BOOL isDir;
    NSFileManager *manager = [NSFileManager defaultManager];
    if ([manager fileExistsAtPath:[self.pathToAppSupport absoluteString] isDirectory:&isDir] && isDir) {
        return YES;
    } else {
        NSError *error = nil;
        [manager createDirectoryAtURL:self.pathToAppSupport withIntermediateDirectories:YES attributes:nil error:&error];
        if (!error) {
            return YES;
        } else {
            NSLog(@"Error creating directory");
            return NO;
        }
    }
}

The above method is fairly straightforward. First, check to see if the path specified in the pathToAppSupport property is a valid directory. If it is a valid directory, return YES. If the path doesn’t exist, then try to create the path. If the attempt succeeds, return YES, otherwise return NO indicating that the Application Support directory does not exist.

Now switch to AppDelegate.m and add the following at the end of applicationDidFinishLaunching::

self.masterViewController.pathToAppSupport = [self applicationFilesDirectory];

The above statement uses a method in the AppDelegate to find the Application Support directory, and then creates a special sub-path specific to your app. This path is then passed on to MasterViewController via the pathToAppSupport property.

Are you wondering when you can actually try out all of the code you’ve been writing? Don’t worry, you’re getting close! :]

Open MasterViewController.xib, and select the NSTableView, being careful to select the table, not the scroll view! Again, check the Document Outline if you’re not sure what is selected, or use the Document Outline to select exactly what you want.

In the Attributes Inspector, change Columns to 2. Then resize the first column so that you see both columns. You can resize the columns by selecting the first column in the Document Outline, and then using the resize handle to drag it to the size you want, as shown below:

Remember that the first column is the one bound to “name,” and the second one is the new, unbound column.

In the Object Library, search for Image Cell. Drag an Image Cell to the new column, as below:

With the second column selected, change the order of the columns by dragging the Image Cell column to be the first column, as such:

With the Image Cell column selected, go the Bindings Inspector and under “Value”, set Model Key Path toimagePath. For Value Transformer, select TableImageCellTransformer. Also ensure that the Bind checkbox is checked, although it should get automatically get checked when you set the Model Key Path, as seen in the following screenshot:

Next, select the detail image view, go to the Bindings Inspector, and set the Model Key Path to imagePathagain. However, set the Value Transformer to DetailImageTransformer, as below:

Now’s your chance to Build and Run the app! :]

If your table is empty, create a bug and give it a name. Click the “Change Picture” button, and find an image you’d like. If you don’t have any other images, there’s always the original lady bug picture in the project folder. Your image will show up in the table cell, and in the detail image as well:

If you’d like to see how the image is saved, switch to Finder, select Go > Go to Folder, and type ~/Library/Application Support/com.razeware.ScaryBugsApp/, which is the Application Support sub-folder where your images will be saved. You’ll see two files: the .storedata file, and a png with a random name:

At this point, you have fully recreated the application from the previous tutorial, but this time using bindings and Core Data. Much easier this way, eh? :]

But wouldn’t it be nice if there were some bugs to view the very first time the app is started, to give the user an idea of what the app looks like, and how it functions?

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 toBugArrayController, 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 ;]

Where To Go From Here?

Here is the complete project from the above tutorial.

The app is complete at this point, but here are a few things that you might want to try as additional challenges:

  • Learn how to use View-based cells with bindings. While it isn’t difficult, it is different from what you’ve done here. And fun :]
  • Add a Search Field to the interface that would allow a user to search the table for bugs. You can do this without adding a single line of code using Bindings. You won’t bind “Value”, but rather, you’ll bind “Predicate.” Leave “Model Key Path” empty; “Predicate Format” is where you will want to use: name contains[c] $value.

I hope you’ve enjoyed learning about Cocoa Bindings and Core Data in Mac apps! If you have any comments or questions, please join the forum discussion below!


This is a blog post by Andy Pereira, a software developer at USAA in San Antonio, TX, and freelance iOS and OS X developer.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值