How to Use Cocoa Bindings and Core Data in a Mac App
2014-05-05 13:36
836 查看
How
to Use Cocoa Bindings and Core Data in a Mac App
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:
Core
Data
Ernesto
García’s Simple Mac App tutorial
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 fullyintended) 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:
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]; } |
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.
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]; |
aPerson.age = @26; |
[aPerson setValue:@26 forKey:@"age"]; |
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: |
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:
Apple’s
Cocoa Bindings Documentation Page
Apple’s
Key-Value Coding Documentation
Apple’s
Key-Value Observing Documentation
Getting Started
In this tutorial, you will continue on with the project from the previoustutorial 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: name, rating,
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’relikely 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; |
self.masterViewController.managedObjectContext = self.managedObjectContext; |
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 |
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" |
-(Bug*)getCurrentBug { if ([[self.bugArrayController selectedObjects] count] > 0) { return [[self.bugArrayController selectedObjects] objectAtIndex:0]; } else { return nil; } } |
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 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 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 astring.
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]]; } } |
transformedValueClass |
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 |
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" |
+(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; } } |
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]; } } |
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; } |
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; |
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; } } |
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]; } } } } |
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; } } } |
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]; |
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"]; } } |
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]; |
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: inAppDelegate.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; |
#import "Bug.h" |
-(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]; } } |
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 confirmationbefore 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 |
#import "Bug.h" |
-(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]; } } |
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" |
@property (strong) IBOutlet NSArrayController *bugArrayController; |
@property (strong) IBOutlet BugArrayController *bugArrayController; |
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 completeproject 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.
相关文章推荐
- How to create aligned partitions in Linux for use with NetApp LUNs, VMDKs, VHDs and other virtual di
- How to use JSon data in mvc action and post form data use JQuery ajax
- How to use insert or retrieve data by using Core Data in iOS
- How to use the System Restore API to save and to restore system data in Visual C++
- How to use Bundle&Minifier and bundleconfig.json in ASP.NET Core
- Core Data on iOS 5 Tutorial: How To Preload and Import Existing Data
- How To Use the ODBC .NET Managed Provider in Visual C# .NET and Connection Strings
- How to use user’s location in your app?
- How to use the Erase Data and Disable Handheld command
- how to use ocx control in console app.
- How to use a 32bit Oracle11_g client in 64 win system and not conflict with sqldeveloper 64 bit tool
- QT29 how to use QWebView and open web page in QWebView
- how to write your annotation types and make use of built-in annotations to control their behavior
- How to use Multinomial and Ordinal Logistic Regression in R ?
- How To Use Animations and Sprite Sheets in Cocos2D
- How to uninstall CUDA driver and toolkit in Mac OS X?
- How to use cocoa pod in your iOS project
- How to use, monitor, and disable transparent hugepages in Red Hat Enterprise Linux 6
- QT30 how to use QcheckBox and QRadioButton in qt
- How to save data in ASCII format in ADS for use in MATLAB