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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Use Cocoa Bindings and Core Data in a Mac App
45 mins
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:
- bind: Know the key you are binding. In this case, you’re binding both “rating” and “editable” respectively.
- toObject: Tell your object what object it’s getting bound to, which is your NSArrayController, bugArrayController.
- 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.
- 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 of NSValueTransformer.
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:
- Create an NSBitmapImageRep from the image passed in.
- 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.
- 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 to imagePath. 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 imagePath again. 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?