Chapter 7. Building a Core Data Application

WHAT'S IN THIS CHAPTER?

  • Fetching data using the NSFetchedResultsController

  • Filtering and sorting your data using NSPredicate and NSSortOrdering

  • Displaying related data using the UITableView

  • Implementing complex validation and default values using custom subclasses of NSManagedObject

In the last chapter, you explored the Xcode Data Modeling tool and learned how to graphically create a data model. In this chapter, you learn how to build a complete data-driven application using Core Data. You will learn how to fetch, filter, and sort your data and display it in a UITableView using the NSFetchedResultsController. You also learn how to modify and delete existing data and take advantage of the relationships that you have defined between your data entities. Finally, you learn how to implement complex validation rules and default values using custom subclasses of NSManagedObject.

I have introduced these topics in the previous chapter. In this chapter, you build a fully functional task manager application while learning how to put the concepts that you learned in the last chapter into action.

THE TASKS APPLICATION ARCHITECTURE

Before you sit down to start coding a new application, it is good to have an idea of what the application will do and how the application will do it. The client on the project typically determines what the application must do. The client should communicate the desired functionality in the form of user requirements or specifications. Usually, you, as the developer, determine how the software will fulfill these requirements.

The application that you build in this chapter will be a task manager like the one that you built in Chapter 4. We will add more functionality to help demonstrate some of the features of Core Data that you learned about in Chapter 5. In this more advanced tasks application, the user should be able to:

  • Create, edit, and delete tasks

  • View overdue tasks in red, sort tasks alphabetically in ascending and descending order, and filter the data to view only high-priority tasks

  • Create task locations, assign a location to a task, and view tasks grouped by their location

  • Display a list of tasks due sooner than a particular task

The Data Model

The first piece of the application is the data model. You need to design a data model that has the entities and attributes needed to implement the functions that I laid out in the previous section. Fortunately, you already did that in Chapter 6. Figure 7-1 shows the data model that you built.

Tasks application data model

Figure 7.1. Tasks application data model

The main entity in this application is the task. Tasks have attributes for due date, priority, and text. There is a transient property that you will calculate at runtime that indicates if a task is overdue. You have also included a fetched property that will list all high-priority tasks. Additionally, you created a relationship to relate a location to a task. Finally, you added a fetch request to determine tasks that are due sooner than the selected task.

The other entity is the location. A task can be marked with a location. Locations have only a name attribute and the task's inverse relationship.

The Class Model

Now that you have a data model, you need to think about how you will design the architecture for the application. The Tasks application that you will build in this chapter consists of a series of table View Controllers and regular View Controllers that a user will use to view, create, and edit tasks. There will also be custom NSManagedObject subclasses that you will use to implement defaulting and data validation.

In Figure 7-2, you can see the class model for the completed Tasks application. This model was generated with Xcode by using Design

The Class Model
Tasks application class Model

Figure 7.2. Tasks application class Model

You may have noticed that the RootViewController is not a UITableViewController. It is a subclass of UIViewController. I did this so that I could embed a UIToolbar at the bottom of the screen for filtering and sorting the data in the table. In summary, any screens that consist solely of a table are subclasses of UITableViewController and screens that contain other controls in addition to the table are subclasses of UIViewController.

Finally, the Location and Task objects are subclasses of NSManagedObject. You will generate these classes from the data model that you built in the last chapter. Then, you will implement custom functionality in the Task class to create default due dates at runtime and to perform single field and multiple field validation.

The User Interface

Now that you have seen the data model and class model for the Tasks application, it is time to look at the user interface. Keep in mind that I designed the interface to provide an example of using Core Data. It is not a model of the most beautiful iPhone application ever built, but it will serve to demonstrate most of the features of Core Data that you will likely use in your own applications.

Figure 7-3 show the UI and the process flow of the application. The RootViewController is the main Tasks screen. This screen displays all of the user's tasks. It also provides a toolbar to perform sorting and filtering of high-priority tasks. There is also a button that will bring the user to the LocationTasksViewController, which displays a list of tasks grouped by location.

Tasks application user interface

Figure 7.3. Tasks application user interface

Tapping the plus button on the top of the RootViewController adds a new task and takes the user to the ViewTaskController. Likewise, tapping on an existing task will also take the user to the ViewTaskController. This screen shows the details of the chosen task or the new task. There are also options on this screen to see all high-priority tasks and to see a list of tasks that are due sooner than the currently selected task.

Tapping a row on the ViewTaskController will take the user to the appropriate editing screen. Aside from allowing the user to select a location for a task, the location selection screen also has the capability to create new locations or delete existing locations. You will see each of these screens in full detail as you build the application.

CODING THE APPLICATION

Now that you are familiar with the basic concepts behind the application, it is time to start writing some code.

In order to complete this application, you will need to do the following:

  1. Build the RootViewController and its interface using Interface Builder.

  2. Generate the NSManagedObject subclasses for use with Core Data.

  3. Implement the ViewTaskController to allow users to create and edit tasks.

  4. Build the sub-screens used to edit the individual task fields.

  5. Implement the filtering and sorting buttons on the toolbar of the RootViewController and the LocationTasksViewController used to view tasks grouped by location.

  6. Implement the advanced features of custom NSManagedObjects in the Task object.

When you are finished, you should have a detailed understanding of how to implement many of the most important features of Core Data. Additionally, you will have a fully featured Core Data–based task management application that you can use to continue experimenting with the features of Core Data. So, let's get started.

ROOTVIEWCONTROLLER AND THE BASIC UI

The first step in creating the Tasks application is to build the RootViewController screen, as shown in Figure 7-4. This is the first screen that the user sees and should contain a list of all of the current tasks. There will be a plus button in the navigation bar used to create new tasks. Additionally, you need a toolbar at the bottom of the screen to allow the user to filter and sort the tasks along with a button to allow the user to bring up the group by location view.

RootViewController screen

Figure 7.4. RootViewController screen

Open up the Tasks project that you created in the previous chapter. Next, double-click on the RootViewController.xib file to open it with Interface Builder.

Note

This book assumes that you already know how to use Interface Builder to create and edit user interfaces for your iPhone applications. If you need a refresher on using Interface Builder, I would recommend that you take a look at James Bucanek's book Professional Xcode 3 (Wrox, 2010), which provides thorough coverage of all of the tools in the Xcode suite, including Interface Builder.

Now, add a UIView object at the root level and move the UITableview that is currently at the root level into the View as a sub-node.

You will need to add a toolbar and its buttons to the interface. Add a UIToolbar control to the view. Next, add four UIBarButtonItem objects to the toolbar so that the toolbar contains five buttons. Open the view in IB. Move the toolbar to the bottom of the view and expand the TableView to fill the rest of the view. Set the title of each UIBarButtonItem to All, Location, Hi-Pri, Asc, or Dsc.

The look of the interface is now complete. The next thing that you need to do is add appropriate outlets and action methods to the RootViewController.h header file. Open RootViewController.h in Xcode. Change the superclass for RootViewController from UITableViewController to UIViewController. This screen will have controls besides the TableView, so it is not appropriate to subclass UITableViewController. The interface declaration should look like this:

@interface RootViewController :
    UIViewController <NSFetchedResultsControllerDelegate>

Add a UITableView instance variable for the taskTableView inside the braces of the interface declaration:

UITableView* taskTableView;

Outside of the interface declaration, add an outlet for the UITableView:

@property (nonatomic, retain) IBOutlet UITableView* taskTableView;

When a user clicks one of the buttons in the toolbar, you need to invoke a method in your code. Therefore, the next step is to add the action methods called when the user clicks on the toolbar buttons:

-(IBAction)toolbarSortOrderChanged:(id)sender;
-(IBAction)toolbarFilterHiPri:(id)sender;
-(IBAction)toolbarFilterAll:(id)sender;
-(IBAction)locationButtonPressed:(id)sender;

The RootViewController header file should look like Listing 7-1.

Example 7.1. RootViewController.h

@interface RootViewController :
    UIViewController <NSFetchedResultsControllerDelegate> {
    NSFetchedResultsController *fetchedResultsController;
    NSManagedObjectContext *managedObjectContext;
    UITableView* taskTableView;
}

-(IBAction)toolbarSortOrderChanged:(id)sender;
-(IBAction)toolbarFilterHiPri:(id)sender;
-(IBAction)toolbarFilterAll:(id)sender;
-(IBAction)locationButtonPressed:(id)sender;

@property (nonatomic, retain)
    NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) IBOutlet UITableView* taskTableView;

@end

Now that the interface and header are ready, you need to get in and modify the RootViewController.m implementation file. First, you'll need to synthesize the new taskTableView property. You can just add the taskTableView to the existing synthesize statement:

@synthesize fetchedResultsController, managedObjectContext,taskTableView;

In the controllerDidChangeContent method, change the reference from self.tableview to self.taskTableView. If you recall, this class used to inherit from UITableViewController. In UITableViewController, there is a property called tableview. Because you are no longer inheriting from UITableViewController, you had to create your own taskTableView property. You are changing the code to reflect this change. The controllerDidChangeContent method should now look like this:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.taskTableView reloadData];
}

Next, although you are not ready to implement the full functionality, you should add stub implementations of the action methods that you declared in the header file. The stubs use the NSLog function to log a message when the user presses a button. This is helpful in debugging issues with Interface Builder because it proves that you have linked the buttons to the methods in Interface Builder. On more than one occasion, I have found myself searching for an elusive bug only to realize that I did not hook up the control to the action method in Interface Builder. Using this easy method, you can see a message in the console any time a button is pressed. The stub code should look like this:

-(IBAction)toolbarSortOrderChanged:(id)sender;
{
    NSLog(@"toolbarSortOrderChanged");
}

-(IBAction)toolbarFilterHiPri:(id)sender{
    NSLog(@"toolbarFilterHiPri");
}

-(IBAction)toolbarFilterAll:(id)sender
{
    NSLog(@"toolbarFilterAll");
}

-(IBAction)locationButtonPressed:(id)sender
{
    NSLog(@"locationButtonPressed");
}

The RootViewController will be displaying data from the Task entity. Therefore, you need to modify the fetchedResultsController accessor method to use the Task entity instead of the default Event entity:

NSEntityDescription *entity =
    [NSEntityDescription entityForName:@"Task"
         inManagedObjectContext:managedObjectContext];
                                                         
RootViewController.h

You will also need to change the sort descriptor to use the Task entity's text attribute instead of timestamp:

NSSortDescriptor *sortDescriptor =
        [[NSSortDescriptor alloc] initWithKey:@"text" ascending:YES];
                                                         
RootViewController.h

The last thing that you will need to do in the implementation file is implement the cleanup methods dealloc and viewDidUnload. It is important to set your properties to nil in viewDidUnload to avoid the possibility of sending messages to objects that you have released. Sending messages to nil is not an error in Objective-C, so setting released pointers to nil is a good thing to do to ensure that your application doesn't crash from sending a message to an invalid pointer.

Because Objective-C on the iPhone is not a garbage-collected language, you are responsible for managing memory yourself. You should release the memory for any properties that you are retaining in the dealloc method. Here is the implementation for the viewDidUnload and dealloc methods:

- (void)viewDidUnload {
    self.managedObjectContext=nil;
    self.fetchedResultsController = nil;
    self.taskTableView=nil;
    [super viewDidUnload];
}

- (void)dealloc {
    [fetchedResultsController release];
    [managedObjectContext release];
    [taskTableView release];
    [super dealloc];
}
                                                         
RootViewController.h

You are finished with the RootViewController for now. The final step is to go back into Interface Builder and hook up the actions and outlets that you created in the RootViewController header to the appropriate interface items.

Open the RootViewController.xib and select File's Owner in the Document window. Using the Connections Inspector window, link the view outlet in File's Owner to the View in the xib. Next, link the taskTableView outlet in File's Owner to the Table View nested under the View. Last, go through each button bar nested under the toolbar and link the selectors in the button bar items to the correct methods in File's Owner. The All button selector should point to the toolbarFilterAll action method, Location should point to locationButtonPressed, Hi-Pri should point to toolbarFilterHiPri, and the Asc and Dsc buttons should point to toolbarSortOrderChanged.

You should now be able to build and run the application. Verify that you receive no errors or warnings during the build process. Once the application comes up in the simulator, click all of the toolbar buttons. Because you added those stub action methods with the NSLog statement, you should be able to quickly verify that you have hooked up the buttons properly in IB by examining the Xcode console. Each time you press a button, the appropriate log message should print.

GENERATING THE MANAGED OBJECT SUBCLASSES

Now that you have built the RootViewController, the next task is to generate your custom NSManagedObject subclasses from the data model. Open the Tasks.xcdatamodel file.

From the File menu, select New File. In the New File dialog box, you should see an option that is not usually there to create a Managed Object Class. Xcode shows this option only when you open this dialog while using the data modeler. After selecting Managed Object Class, click Next. You will see a screen that lets you pick the data model entities for which you want to generate code. Select the Location entity. Make sure that you have selected the "Generate accessors" and "Generate Obj-C 2.0 Properties" checkboxes and click Finish.

Repeat the process to create the Task object. You would think that you could just check both the Task and Location entities the first time through to generate classes for both entities. Generally, you would be correct. However, because there is a dependency between the Task and Location objects, if you generate both classes at the same time and Xcode generates the Task class first, the tool will not create the correct reference in the Task header for the type (Location*) of the location property. This appears to be a bug in the tool.

When you have finished generating the classes, go back into Xcode. In the left pane, create a new group under Classes called Managed Objects. Groups are like folders and can help you to keep your project organized. Move the Task.m, Task.h, Location.m, and Location.h files into the new Managed Objects group.

Open the header for the Task class, Task.h. Add a property for the highPriTasks fetched property and the isOverdue dynamic property:

@property (nonatomic,retain) NSArray* highPriTasks;
@property (nonatomic, retain) NSNumber * isOverdue;
                                                         
GENERATING THE MANAGED OBJECT SUBCLASSES

Finally, in Task.m, add an @dynamic statement for the highPriTasks fetched property and isOverdue property to tell the compiler that the framework will dynamically generate these properties at runtime:

@dynamic highPriTasks;
@dynamic isOverdue;
                                                         
GENERATING THE MANAGED OBJECT SUBCLASSES

Add a method stub for the isOverdue getter function to return NO. You will implement the actual function later on in the chapter:

- (NSNumber*) isOverdue
{
    BOOL isTaskOverdue = NO;

    return [NSNumber numberWithBool:isTaskOverdue];
}

ADDING AND VIEWING TASKS

Now that you have built the main screen where a user can add and select tasks, you need to build a way for your users to view and edit tasks. You will implement this functionality with the ViewTaskController. You can see the interface for the ViewTaskController in Figure 7-5.

The ViewTaskController

Figure 7.5. The ViewTaskController

Building the ViewTaskController

In Xcode, create a new UIViewController subclass called ViewTaskController. Make sure that the UITableViewController subclass option is selected and that "With XIB for user interface" is not selected.

In the ViewTaskController.h header file, add imports for the Task.h and Location.h headers:

#import "Task.h"
#import "Location.h"
                                                         
Building the ViewTaskController

In the interface section, add a member variable to hold an instance of the managed object context. Add another member to hold a Task object:

NSManagedObjectContext *managedObjectContext;
    Task* managedTaskObject;
                                                         
Building the ViewTaskController

Add properties for both of the member variables that you added previously:

@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) Task* managedTaskObject;
                                                         
Building the ViewTaskController

The completed header should look like Listing 7-2.

Example 7.2. ViewTaskController.h

#import <UIKit/UIKit.h>
#import "Task.h"
#import "Location.h"

@interface ViewTaskController : UITableViewController {
    NSManagedObjectContext *managedObjectContext;
    Task* managedTaskObject;

}

@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) Task* managedTaskObject;

@end

You will add code to the RootViewController to create an instance of the ViewTaskController when a user selects a row in the table or when a user clicks the plus sign to add a new task. You will then populate the ViewTaskController properties with a pointer to the managed object context and a pointer to the Task object that the ViewTaskController will display.

This design prevents the ViewTaskController from having to know anything about the class that is calling it. The ViewTaskController doesn't need to know how to find the Task object that it will display. This prevents the ViewTaskController from needing a reference to the context. In general, it is a good practice to pass in all of the data that an object needs to function. This loosely couples the class to other classes in the application.

Sure, the ViewTaskController could have obtained a pointer to the managed object context from the app delegate, but that would tightly couple it to this application. A more generic and reusable design is to build the controller such that it has all of the information that it needs to operate without having to look outside of the class.

Now that you have the ViewTaskController header coded, it's time to move on to the implementation. Open the ViewTaskController.m implementation file. Synthesize the properties that you declared in the header file:

@synthesize managedObjectContext, managedTaskObject;

Next, uncomment the viewDidLoad method and add a line of code to set the title of the screen. The Nav Bar control displays this title at the top of the screen as well as in the Back button on subsequent screens. The viewDidLoad method should look like this:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Uncomment the following line to display an Edit button in the navigation
    // bar for this view controller.
    self.navigationItem.title = @"Task Detail";
}
                                                         
ViewTaskController.h

Now, uncomment the viewWillAppear method and add a line of code to reload the data in the tableView. The viewWillAppear method should look like this:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    // Reload the data for the table to refresh from the context
    [self.tableView reloadData];

}
                                                         
ViewTaskController.h

You need to add code to clean up the memory used by the instance variables and properties defined in the ViewTaskController just as you did in RootViewController. Implement viewDidUnload and dealloc to free the memory and set the properties used by the class to nil:

- (void)viewDidUnload {
    self.managedObjectContext=nil;
    self.managedTaskObject = nil;
    [super viewDidUnload];
}
- (void)dealloc {
    [managedObjectContext release];
    [managedTaskObject release];
    [super dealloc];
}
                                                         
ViewTaskController.h

Because you implemented the ViewTaskController as a UITableViewController, you need to implement the TableView methods as you learned in the previous chapters on using the UITableView control.

You can leave the numberOfSectionsInTableView method alone because the table will display only one section.

You will need to modify the tableView:numberOfRowsInSection: method to return six rows. You will populate each of these six rows with data in the tableView:cellForRowAtIndexPath: method. The tableView:numberOfRowsInSection: method should look like this:

// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {
    return 6;
}
                                                         
ViewTaskController.h

Next, you will implement the tableView:cellForRowAtIndexPath: method to display the appropriate content for each row in the table. Building this table will be a little different from what you have seen before, as you are not building each row dynamically based on its content as you have done in the past. Each row will have a static label based on the row number and will have some dynamic content taken from the Task object that corresponds with that row.

Here is the code for the TableView:cellForRowAtIndexPath: method:

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
                 initWithStyle:UITableViewCellStyleValue2
reuseIdentifier:CellIdentifier] autorelease];
    }

    // Set up the cell...
    switch (indexPath.row) {
        case 0:
            cell.textLabel.text = @"Text";
            cell.detailTextLabel.text = managedTaskObject.text;
            break;
        case 1:
            cell.textLabel.text = @"Priority";

            // Get the priority number and convert it to a string
            NSString* priorityString=nil;

            switch ([managedTaskObject.priority intValue]) {
                case 0:
                    priorityString = @"None";
                    break;
                case 1:
                    priorityString = @"Low";
                    break;
                case 2:
                    priorityString = @"Medium";
                    break;
                case 3:
                    priorityString = @"High";
                    break;
                default:
                    break;
            }

            cell.detailTextLabel.text = priorityString;
            [priorityString release];

            break;
        case 2:
            cell.textLabel.text = @"Due Date";

            //  Create a date formatter to format the date from the picker
            NSDateFormatter* df = [[NSDateFormatter alloc] init];
            [df setDateStyle:NSDateFormatterLongStyle];
            cell.detailTextLabel.text =
                [df stringFromDate:managedTaskObject.dueDate ];
            [df release];

            break;
        case 3:
            cell.textLabel.text = @"Location";
            Location* locationObject = managedTaskObject.location;
            if (locationObject!=nil)
            {
                cell.detailTextLabel.text = locationObject.name;
            }
else {
                cell.detailTextLabel.text = @"Not Set";

            }

            break;
        case 4:
            // Show hi-pri tasks alert
            cell.detailTextLabel.text = @"Hi-Pri Tasks";
            break;
        case 5:
            // Show sooner tasks alert
            cell.detailTextLabel.text = @"Tasks due sooner than this one";
            break;

        default:
            break;
    }

    return cell;
}
                                                         
ViewTaskController.h

The first portion of this code should be familiar. It tries to dequeue a cell, and if it cannot, it creates a new cell.

The rest of the code executes a switch statement to determine the content of the row based on which row the TableView requests.

The first row of the table will display a label that says "Text" and the text attribute from the Task object.

Row two displays the label "Priority" and then converts the integer priority from the Task object into a priority string that the TableView displays in the cell.

The next row displays the "Due Date" label and uses an NSDateFormatter to convert the NSDate object stored in the managed object into a string. You can use one of the pre-defined formats or you can define your own. For more information on using NSDateFormatter, look at the Xcode SDK documentation or browse to http://developer.apple.com/iphone/library/documentation/Cocoa/Reference/Foundation/Classes/NSDateFormatter_Class/Reference/Reference.html.

The fourth row displays the Location label. Then, the code tries to get a Location object from the Task's location property. The code displays the name property of the Location object, if it exists. If not, the code displays "Not Set."

The final two cases display labels to inform the user that tapping these cells will bring up a list of high-priority tasks or a list of tasks that are due sooner than the currently displayed task. You will implement didSelectRowAtIndexPath to do something when the user taps these rows later in the chapter.

Leave the default implementation of the tableView:commitEditingStyle:forRowAtIndexPath: and tableView:canMoveRowAtIndexPath: methods.

Changes to the RootViewController

Now that you have built the ViewTaskController, you need to make some changes to the RootViewController to access the new screen.

First, you will configure the RootViewController navigation bar. In the viewDidLoad method of the RootViewController.m implementation file, remove the line:

self.navigationItem.leftBarButtonItem = self.editButtonItem;

This screen will not be using the Edit button.

At the end of viewDidLoad, add the following line of code to set the title of the screen in the navigation bar:

self.title = @"Tasks";

You will also need to add import statements to import the headers for the Location and Task objects as well as the ViewTaskController. Add the following imports to the top of the RootViewController implementation file:

#import "ViewTaskController.h"
#import "Location.h"
#import "Task.h"
                                                         
Changes to the RootViewController

Next, you need to implement the insertNewObject method. This method creates a new Task object and then passes control off to the ViewTaskController to edit the new task. Tapping the plus button in the navigation bar calls the insertNewObject method. Here is the insertNewObject method:

- (void)insertNewObject {

    NSManagedObjectContext *context = self.managedObjectContext;

    Task *newTask =
    [NSEntityDescription insertNewObjectForEntityForName:@"Task"
                                  inManagedObjectContext:context];

    ViewTaskController* taskController =
        [[ViewTaskController alloc] initWithStyle:UITableViewStyleGrouped];
    taskController.managedTaskObject=newTask;
    taskController.managedObjectContext = self.managedObjectContext;

    [self.navigationController pushViewController:taskController animated:YES];

    [taskController release];
                                                         
Changes to the RootViewController

This method is straightforward. First, you use the context to create a new Task object. Then, you create an instance of the ViewTaskController and populate its managedTaskObject and managedObjectContext properties. Last, you push the new ViewTaskController onto the navigation stack and then release it.

The last change in the RootViewController is to implement the TableView methods. You can leave the default numberOfSectionsInTableView:, tableView:numberOfRowsInSection:, tableView:commitEditingStyle:forRowAtIndexPath: and tableView:canMoveRowAtIndexPath: methods.

You do need to implement the tableView:cellForRowAtIndexPath: to display the text property of the Task object for the row. You will also add some code to check the isOverdue transient property and display overdue tasks in red. Here is the code for tableView:cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
    }

    // Configure the cell.
    Task *managedTaskObject =
        [fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = managedTaskObject.text;

    // Change the text color if the task is overdue
    if (managedTaskObject.isOverdue==[NSNumber numberWithBool: YES])
    {
        cell.textLabel.textColor = [UIColor redColor];
    }
    else {
        cell.textLabel.textColor = [UIColor blackColor];
    }

    return cell;
}
                                                         
Changes to the RootViewController

You should be familiar with how this code works. The beginning of the method tries to dequeue a cell and if it cannot, it creates a new one. Next, the code gets the Task object from the fetchedResultsController that corresponds to the requested cell. Then, the cell's textLabel is set with the text from the Task object. Finally, use the value of the Task's isOverdue property to determine the color of the text.

Build and run the application. You should not get any errors or warnings. The application should now allow you to create new default tasks and then navigate to the ViewTaskController screen. You should also be able to select tasks in the Tasks screen and view them in the ViewTaskController screen.

BUILDING THE EDITING CONTROLLERS

Your Tasks application now has the capability to create new tasks, and view existing tasks. Although there is some decent functionality there, the application is useless without being able to edit the contents of your tasks. In this section, you will implement the screens shown in Figure 7-6. These screens will allow the user to edit each individual piece of data in a task. Create a new group under Classes called Sub Controllers to hold all of your edit controller code.

Task data editing screens

Figure 7.6. Task data editing screens

Editing Text with the EditTextController

The EditTextController screen, as you can see in Figure 7-7, will allow the user to edit the text used in the Task and Location objects. The screen consists of a UITableView with one cell. Embedded in that cell is a UITextField. This design is consistent with the behavior of the text editing screens in the stock iPhone applications such as Contacts.

In the new Sub Controllers group, create a new UITableviewController without NIB called EditTextController. Open the EditTextController.h header file and add instance variables for an NSManagedObject and the NSManagedObjectContext:

NSManagedObject* managedObject;
NSManagedObjectContext *managedObjectContext;
                                                         
Editing Text with the EditTextController
EditTextController Screen

Figure 7.7. EditTextController Screen

The parent screen will set the managed object and the context before it pushes the EditTextController on to the navigation stack.

You also need to add an instance variable to hold an NSString* called keyString. Because this screen supports editing both the text property of the Task object and the name property of the Location object, you will use key-value coding (KVC) to take the text entered on the screen and update the managed object. This is also the reason that the screen accepts an NSManagedObject instead of one of your custom subclasses. That way, the screen is generic enough to edit text fields on any Managed Objects and is not limited to editing only Task or Location objects. Add the keyString member variable to the following header:

NSString* keyString;

Finally, add a member variable to hold the UITextField that you will embed in the TableView:

UITextField* textField;

The last thing that you need to do is add property declarations for your instance variables. Your finished header file should look like Listing 7-3.

Example 7.3. EditTextController.h

#import <UIKit/UIKit.h>

@interface EditTextController : UITableViewController {
NSManagedObject* managedObject;
    NSManagedObjectContext *managedObjectContext;
    NSString* keyString;

    UITextField* textField;
}

@property (nonatomic, retain) NSManagedObject* managedObject;
@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) NSString* keyString;

@end

Now, you have to work on the implementation file, EditTextController.m. The first thing that you will need to do is synthesize the properties that you declared in the header:

@synthesize managedObject,keyString,managedObjectContext;

Next, uncomment and implement the viewDidLoad method:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Create the textfield
    textField = [[UITextField alloc] initWithFrame:CGRectMake(10, 10, 200, 20)];

    // Notice how you use KVC here because you might get a Task or a Location in
    // this generic text editor
    textField.text = [managedObject valueForKey:keyString];
    textField.clearsOnBeginEditing=YES;

    // Add the save button
    UIBarButtonItem* saveButton =[[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemSave
                                  target:self
                                  action:@selector (saveButtonPressed:)];

    self.navigationItem.rightBarButtonItem = saveButton;
    [saveButton release];
}
                                                         
EditTextController.h

The first thing that you need to do in viewDidLoad is call super viewDidLoad. In general, you want to call the superclass version of a method before you do anything in your method to ensure that everything in the superclass is set up properly before you begin your work. Conversely, you may have noticed that in viewDidUnload and dealloc, you do your work first and then call the superclass version of those methods at the end.

Next, you move on to create and configure the textfield instance variable. The code sets the text of the textfield using key-value coding, not the specific text or name property of the Task or Location object. Remember that you are building this screen generically to be able to handle the text input for any field in any managed object.

Last, the code creates the Save button and sets it as the rightBarButtonItem in the nav bar.

The next task is to implement the saveButtonPressed method. Pressing the Save button in the nav bar calls the saveButtonPressed method. In this method, you will get the text from the text field and use KVC to set the appropriate key in the managed object. Remember that the previous screen set the keyString before displaying the EditTextController. Then, you save the context and pop the View Controller off the navigation stack. Here is the code:

-(void) saveButtonPressed: (id) sender
{

    // Configure the managed object
    // Notice how you use KVC here because you might get a Task or a Location
    // in this generic text editor
    [managedObject setValue:textField.text forKey:keyString];

    // Save the context.
    NSError *error = nil;
    if (![self.managedObjectContext save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    // pop the view
    [self.navigationController popViewControllerAnimated:YES];

}
                                                         
EditTextController.h

In the viewDidUnload method, set the properties of the class to nil:

- (void)viewDidUnload {
    self.managedObjectContext=nil;
    self.managedObject = nil;
    self.keyString=nil;
    [super viewDidUnload];
}
                                                         
EditTextController.h

Leave the numberOfSectionsInTableView method with the default implementation. The table will have only one section. Change the tableView:numberOfRowsInSection: method to return one row:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {
    return 1;
}
                                                         
EditTextController.h

Next, you should implement the tableView:cellForRowAtIndexPath: method to show the textField in the tableView cell:

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
    }

    // Set up the cell...
    if (indexPath.row == 0)
    {
        UIView* cv =  cell.contentView;

        [cv addSubview:textField];

    }

    return cell;
}
                                                         
EditTextController.h

Implement tableView:didSelectRowAtIndexPath: to deselect the selected cell. You don't necessarily have to do this to complete the functionality of your application, but the Apple Human Interface Guidelines suggest that you deselect a tableview cell after its selection. Therefore, I'm including the following code:

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    //  Deselect the currently selected row according to the HIG
    [tableView deselectRowAtIndexPath:indexPath animated:NO];
}
                                                         
EditTextController.h

Last, but not least, you need to implement the dealloc method to clean up any memory that your class has allocated:

- (void)dealloc {
    [managedObject release];
    [managedObjectContext release];
    [keyString release];
    [super dealloc];
}
                                                         
EditTextController.h

Setting Priorities with the EditPriorityController

The EditPriorityController screen, which you can see in Figure 7-8, allows the user to choose the priority for a task. Again, you will implement the screen as a TableView. This time, there will be a row for each priority level. In the Sub Controllers group, create a new UITableviewController without a NIB called EditPriorityController.

In the header file, you will need to add instance variables and properties for a Task object and the context. You will also need to add a #import directive for the Task.h header file. Your header should look like Listing 7-4.

EditPriorityController Screen

Figure 7.8. EditPriorityController Screen

Example 7.4. EditPriorityController.h

#import <UIKit/UIKit.h>
#import "Task.h"


@interface EditPriorityController : UITableViewController {
Task* managedTaskObject;
    NSManagedObjectContext *managedObjectContext;

}

@property (nonatomic, retain) Task* managedTaskObject;
@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;


@end

In the implementation file, you first need to synthesize the properties that you declared in the header:

@synthesize managedTaskObject,managedObjectContext;

As in the EditTextController, you should implement the viewDidUnload method to set the properties defined in the class to nil:

- (void)viewDidUnload {
    self.managedObjectContext=nil;
    self.managedTaskObject = nil;
    [super viewDidUnload];
}
                                                         
EditPriorityController.h

Leave the numberOfSectionsInTableView method with the default implementation because the table will have only one section. Change tableView:numberOfRowsInSection: to return four rows, one for each priority level.

Next, implement the tableView:cellForRowAtIndexPath: method to show the priority options in the appropriate cell:

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
    }
// Set up the cell...
    switch (indexPath.row) {
        case 0:
            cell.textLabel.text = @"None";
            break;
        case 1:
            cell.textLabel.text = @"Low";
            break;
        case 2:
            cell.textLabel.text = @"Medium";
            break;
        case 3:
            cell.textLabel.text = @"High";
            break;
        default:
            break;
    }

    // place the checkmark next to the existing priority
    if (indexPath.row == [managedTaskObject.priority intValue] )
    {
        cell.accessoryType=UITableViewCellAccessoryCheckmark;
    }

    return cell;
}
                                                         
EditPriorityController.h

This method should be familiar to you. The first few lines try to dequeue a cell as usual. Then, the code determines the text of the cell based on which cell you are providing. The last bit of code displays a checkmark next to the currently chosen priority for the task.

When a user taps a row, you need to save that selection in the Task object. You will do that in the tableView:didSelectRowAtIndexPath: method:

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    //  Deselect the currently selected row according to the HIG
    [tableView deselectRowAtIndexPath:indexPath animated:NO];

    // Configure the managed object
    managedTaskObject.priority=[NSNumber numberWithInt:indexPath.row];

    // Save the context.
    NSError *error = nil;
    if (![self.managedObjectContext save:&error]) {
        // There was an error validating the date
// Display error information

                NSLog(@"Unresolved error %@, %@", error, [error userInfo]);

        UIAlertView* alert = [[UIAlertView alloc]
            initWithTitle:@"Invalid Due Date"
            message:[[error userInfo] valueForKey:@"ErrorString"]
            delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil ];
        [alert show];
        [alert release];

        // Roll back the context to
        // revert back to the old priority
        [self.managedObjectContext rollback];


    }
    else {
        // pop the view
        [self.navigationController popViewControllerAnimated:YES];

    }
}
                                                         
EditPriorityController.h

The first thing that this method does is deselect the selected row as I explained in the last section.

The next line sets the Task object's priority field to the priority level selected on the screen. Then, the code saves the context. Because you are going to add a validation rule that includes the priority, there is a possibility that the new priority could fail the validation. If the validation fails, the save method will fail, so you need to roll the context back to its state before the failed save. If the save method fails, you revert the priority back to its original state using the rollback method of the context. The rollback method undoes all changes to the context that have not yet been committed with a successful save call. If an error occurs, such as a validation failure, you show the user an alert to inform him that a problem has occurred. If there is no problem, the code pops the View Controller from the stack.

Finally, implement the dealloc method to release the member variables that you allocated in the class:

- (void)dealloc {
    [managedTaskObject release];
    [managedObjectContext release];
    [super dealloc];
}
                                                         
EditPriorityController.h

Adding and Editing Locations with the EditLocationController

The user navigates to the EditLocationController by tapping the location cell on the ViewTaskController. The EditLocationController, as shown in Figure 7-9, allows the user to select a location, add new locations, and delete existing locations. To create the EditLocationController, create a new UITableviewController without a NIB called EditLocationController.

Modify your new header file to create instance variables and properties to hold the context and Task objects that the parent screen will configure. You will also need to add a member variable and property for the NSFetchedResultsController that you will use to display your location list. Additionally, you will need to add #import directives for the Task and Location header files. The completed header file should look like Listing 7-5.

EditLocationController Screen

Figure 7.9. EditLocationController Screen

Example 7.5. EditLocationController.h

#import <UIKit/UIKit.h>
#import "Task.h"
#import "Location.h"

@interface EditLocationController :
    UITableViewController <NSFetchedResultsControllerDelegate> {

    NSFetchedResultsController *fetchedResultsController;
    NSManagedObjectContext *managedObjectContext;
    Task* managedTaskObject;
}

@property (nonatomic, retain) NSFetchedResultsController
                                    *fetchedResultsController;
@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) Task* managedTaskObject;

@end

Let's move on to the implementation file. Add an import statement for EditTextController.h:

#import "EditTextController.h"

You will use the EditTextController to add the text for newly added locations. Next, synthesize the properties that you declared in the header file:

@synthesize fetchedResultsController, managedObjectContext, managedTaskObject;

Uncomment and implement the viewDidLoad method:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Set up the add button
    UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
        target:self action:@selector(insertNewLocation)];

    self.navigationItem.rightBarButtonItem = addButton;
    [addButton release];

    NSError* error;

    if (![[self fetchedResultsController] performFetch:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    // set the title to display in the nav bar
    self.title = @"Location";

}
                                                         
EditLocationController.h

This code creates the addButton and sets it to call the insertNewLocation method. It then adds the addButton to the nav bar. Next, you tell the fetched results controller to fetch its data. Finally, you set the title of the screen to Location.

Next, you need to set the class properties in the viewDidUnload method to nil:

- (void)viewDidUnload {
    self.fetchedResultsController=nil;
    self.managedTaskObject=nil;
    self.managedObjectContext=nil;
    [super viewDidUnload];
}
                                                         
EditLocationController.h

Now, add the insertNewLocation method that runs when the user taps the Add button that you created in viewDidLoad. This method adds a new Location to the context and pushes the text controller on to the navigation stack to allow the user to edit the location name. Here is the insertNewLocation method:

- (void)insertNewLocation {

    NSManagedObjectContext *context = self.managedObjectContext;

    Location *newLocation =
[NSEntityDescription insertNewObjectForEntityForName:@"Location"
                                  inManagedObjectContext:context];

    EditTextController* textController =
        [[EditTextController alloc] initWithStyle:UITableViewStyleGrouped];
    textController.managedObject=newLocation;
    textController.managedObjectContext = self.managedObjectContext;
    textController.keyString=@"name";

    [self.navigationController pushViewController:textController animated:YES];

    [textController release];
}
                                                         
EditLocationController.h

Next, implement the fetched results controller accessor method to fetch the Location entities and sort them in ascending order by name:

- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    // Set up the fetched results controller.
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity =
        [NSEntityDescription
            entityForName:@"Location"
            inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];

    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor =
        [[NSSortDescriptor alloc]
            initWithKey:@"name"
            ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc]
                                initWithObjects:sortDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController =
        [[NSFetchedResultsController alloc]
            initWithFetchRequest:fetchRequest
            managedObjectContext:managedObjectContext
            sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    [aFetchedResultsController release];
    [fetchRequest release];
    [sortDescriptor release];
    [sortDescriptors release];

    return fetchedResultsController;
}
                                                         
EditLocationController.h

Implement the controllerDidChangeContent delegate method to reload the table data:

// NSFetchedResultsControllerDelegate method to notify the delegate
// that all section and object changes have been processed.
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {

    [self.tableView reloadData];
}
                                                         
EditLocationController.h

Change the numberOfSectionsInTableView and tableView:numberOfRowsInSection: methods to use the fetchedResultsController:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[fetchedResultsController sections] count];
}

// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {

    id <NSFetchedResultsSectionInfo> sectionInfo =
        [[fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}
                                                         
EditLocationController.h

Now, implement the tableView:cellForRowAtIndexPath: method to show the locations from the fetched results controller:

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
    }

    Location *managedLocationObject =
        [fetchedResultsController objectAtIndexPath:indexPath];

    // If the location in the task object is the same as the location object
    // draw the checkmark
    if (managedTaskObject.location == managedLocationObject)
    {
        cell.accessoryType=UITableViewCellAccessoryCheckmark;

    }

    cell.textLabel.text = managedLocationObject.name;

    return cell;
}
                                                         
EditLocationController.h

You can see that the code does the usual cell setup and dequeuing. Then, it obtains a Location object for the cell from the fetchedResultsController. The code then checks to see if the location that it will use is also the location in the Task. If it is, the code displays the checkmark accessory for the cell. Last, you use the name property of the Location object as the cell text.

Next, you need to implement tableView:didSelectRowAtIndexPath: to save the selected location:

tableView:didSelectRowAtIndexPath: to save off the selected location
- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    //  Deselect the currently selected row according to the HIG
    [tableView deselectRowAtIndexPath:indexPath animated:NO];

    // set the Task's location to the chosen location
    Location *newLocationObject =
        [fetchedResultsController objectAtIndexPath:indexPath];

    managedTaskObject.location=newLocationObject;

    // Save the context.
    NSError *error = nil;

    if (![self.managedObjectContext save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
else {

        // pop the view
        [self.navigationController popViewControllerAnimated:YES];
    }
}
                                                         
EditLocationController.h

This code gets the selected Location object from the fetchedResultsController, sets the location in the Task object, saves the context, and pops the view from the navigation stack.

In order to allow the user to delete locations, uncomment and implement the tableView:commitEditingStyle: method:

// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
    forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the managed object for the given index path
        NSManagedObjectContext *context =
            [fetchedResultsController managedObjectContext];

        [context deleteObject:[fetchedResultsController
                               objectAtIndexPath:indexPath]];

        // Save the context.
        NSError *error = nil;
        if (![context save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}
                                                         
EditLocationController.h

This code enables the delete editing style that displays the Delete button when a user swipes across a row. The code then gets the context, deletes the Location that the user selected, and saves the context.

Finally, implement dealloc to release your properties:

- (void)dealloc {
    [fetchedResultsController release];
    [managedObjectContext release];
    [managedTaskObject release];
    [super dealloc];
}
                                                         
EditLocationController.h

Modifying Dates with the EditDateController

The last edit controller that you will implement is the EditDateController, which you can see in Figure 7-10. Like the RootViewController, you will need to create the interface for this controller using Interface Builder and a NIB because the screen will need to hold a UIDatePicker control in addition to the TableView that will display the selected date.

Create a new UIViewController with NIB called EditDateController. Make sure that you have unchecked "UITableViewController subclass" and that you have checked "With XIB for user interface." I like to keep my projects organized, so I moved the XIB file into the Resources folder.

In the EditDateController.h header file, add an import statement for your Task object:

#import "Task.h"

Add references to the UITableViewDelegate and UITableViewDataSource protocols in the interface definition to indicate that you plan to implement these protocols:

@interface EditDateController :
    UIViewController <UITableViewDelegate, UITableViewDataSource>
Edit DateController Screen

Figure 7.10. Edit DateController Screen

Add member variables and properties for a Task object, the context, a UITableView, and a UIDatePicker. Finally, add an action method, dateChanged, that will run when the user changes the date in the UIDatePicker. The finished header should look like Listing 7-6.

Example 7.6. EditDateController.h

#import <UIKit/UIKit.h>
#import "Task.h"

@interface EditDateController :
    UIViewController <UITableViewDelegate, UITableViewDataSource>
{
    Task* managedTaskObject;
    NSManagedObjectContext *managedObjectContext;
    UIDatePicker* datePicker;
    UITableView* tv;
}

@property (nonatomic, retain) Task* managedTaskObject;
@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) IBOutlet UIDatePicker* datePicker;
@property (nonatomic, retain) IBOutlet UITableView* tv;

-(IBAction)dateChanged:(id)sender;

@end

Make sure that you save your completed header. You need to save because Interface Builder will only read the last saved version of the header, and you need IB to see your DatePicker, TableView, and dateChanged methods.

Now it's time to move into Interface Builder and work on the UI. Double-click on the EditDateController.xib file to open it using IB. Drag a UIDatePicker control into the view widow and place it at the bottom of the view. In the attributes inspector for the DatePicker, set the Mode to Date. There are various date and time display modes, but you only need to allow the user to select a date.

Drag a UITableView into the view window and position it at the top of the view. Stretch the TableView to fill the whole screen. Then send it to the back using Layout

EditDateController.h

Next, you need to connect the interface to the code class. Hook up the TableView's dataSource and delegate to File's Owner. Hook up the File's Owner tv variable to the TableView and datePicker variable to the UIDatePicker. Finally, hook up the UIDatePicker's Value Changed action to the File's Owner dateChanged: method.

You can now move on to the EditDateController.m implementation file. First, you need to synthesize the properties that you defined in the header:

@synthesize managedTaskObject,managedObjectContext,datePicker,tv;

Uncomment and implement the viewDidLoad method:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Add the save button
    UIBarButtonItem* saveButton =
        [[UIBarButtonItem alloc]
         initWithBarButtonSystemItem:UIBarButtonSystemItemSave
         target:self
         action:@selector (saveButtonPressed:)];
    self.navigationItem.rightBarButtonItem = saveButton;
    [saveButton release];

    // Set the date to the one in the managed object, if it is set
    // else, set it to today
    NSDate* objectDate = managedTaskObject.dueDate;
    if (objectDate!=nil)
    {
        datePicker.date = objectDate;
    }
    else {
        datePicker.date = [NSDate date];
    }
}
                                                         
EditDateController.h

This method creates the Save button and adds it to the nav bar. It also sets the date in the date picker control to the date in the Task object, if it exists, or to the current date.

Implement the viewDidUnload method to set the class's properties to nil:

- (void)viewDidUnload {
    self.managedObjectContext=nil;
    self.managedTaskObject = nil;
    self.datePicker = nil;
    self.tv=nil;
    [super viewDidUnload];
}
                                                         
EditDateController.h

The user invokes the saveButtonPressed method when she clicks the Save button created in viewDidLoad. You need to implement saveButtonPressed to save the currently selected date to the Task object. Here is the code:

-(void) saveButtonPressed: (id) sender
{

    // Configure the managed object
    managedTaskObject.dueDate=[datePicker date];

    // Save the context.
    NSError *error = nil;
    if (![self.managedObjectContext save:&error]) {
        // There was an error validating the date
        // Display error information
        UIAlertView* alert =
            [[UIAlertView alloc]
             initWithTitle:@"Invalid Due Date"
             message:[[error userInfo] valueForKey:@"ErrorString"]
             delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil ];
        [alert show];
        [alert release];

        // Roll back the context to
        // revert back to the original date
        [self.managedObjectContext rollback];

    }
    else{
        // pop the view
        [self.navigationController popViewControllerAnimated:YES];
    }
}
                                                         
EditDateController.h

Because you are going to add a validation rule that includes the date, there is a possibility that the new date can fail the validation. If the validation fails, the save method will fail, so you need to roll the context back to its old state before the failed save. If the save method fails, you revert the dueDate back to the originally selected date by calling the rollback method of the context. Then, you show an alert to the user. If the save is successful, you simply pop the controller from the navigation stack.

Next, implement the dateChanged method to reload the TableView and update the date text:

-(IBAction)dateChanged:(id)sender{
    //  Refresh the date display
    [tv reloadData];
}
                                                         
EditDateController.h

Because there is only one cell in the TableView, implement the ViewTaskController screen and tableView:numberOfRowsInSection: to return 1:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {

    return 1;
}
                                                         
EditDateController.h

Implement tableView:cellForRowAtIndexPath: to show the date chosen in the DatePicker:

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell =
            [[[UITableViewCell alloc]
              initWithStyle:UITableViewCellStyleDefault
              reuseIdentifier:CellIdentifier] autorelease];
    }

    // Set up the cell...
    if (indexPath.row == 0)
    {
//  Create a date formatter to format the date from the picker
        NSDateFormatter* df = [[NSDateFormatter alloc] init];
        [df setDateStyle:NSDateFormatterLongStyle];
        cell.textLabel.text = [df stringFromDate:datePicker.date ];
        [df release];
    }

    return cell;
}
                                                         
EditDateController.h

You can see that you are once again using an NSDateFormatter to convert the NSDate object into a string for display in the TableViewCell.

In the dealloc method, release the member variables that correspond to the class properties:

- (void)dealloc {
    [managedTaskObject release];
    [managedObjectContext release];
    [datePicker release];
    [tv release];
    [super dealloc];
}
                                                         
EditDateController.h

Finishing Up the Editing Controllers

You have now finished implementing all of the edit controllers. The last thing that you need to do before you are ready to run the program is go back and add code to the ViewTaskController.m to use the new subcontrollers to edit the task data.

In ViewTaskController.m, add an import statement for each subcontroller:

#import "EditTextController.h"
#import "EditPriorityController.h"
#import "EditDateController.h"
#import "EditLocationController.h"
                                                         
Finishing Up the Editing Controllers

You will also need to add an import for the App delegate because you need to get a reference to the managedObjectModel in order to use your stored fetch request:

#import "TasksAppDelegate.h"

You can now implement the didSelectRowAtIndexPath method that you left out when you were implementing the ViewTaskController earlier in the chapter. This method runs when a user selects a row in the table. The method should display the correct edit View Controller based on which row the user selects.

The last two buttons in the table do not use edit View Controllers. The Hi-Pri Tasks button demonstrates how to use a fetched property to get a list of high-priority tasks. The "Tasks due sooner" button shows you how to use a stored fetch request.

The following is the code for didSelectRowAtIndexPath:

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    //  Deselect the currently selected row according to the HIG
    [tableView deselectRowAtIndexPath:indexPath animated:NO];

    //  Based on the selected row, choose which controlelr to push
    switch (indexPath.row) {
        case 0:
        {
            EditTextController* etc = [[EditTextController alloc]
                                      initWithStyle:UITableViewStyleGrouped];
etc.managedObject = self.managedTaskObject;
            etc.keyString=@"text";
            etc.managedObjectContext = self.managedObjectContext;
            [self.navigationController pushViewController:etc animated:YES];
            [etc release];
            break;
        }
        case 1:
        {
            EditPriorityController* epc =
                [[EditPriorityController alloc]
                 initWithStyle:UITableViewStyleGrouped];
            epc.managedTaskObject = self.managedTaskObject;
            epc.managedObjectContext = self.managedObjectContext;

            [self.navigationController pushViewController:epc animated:YES];
            [epc release];
            break;
        }
        case 2:
        {
            EditDateController* edc = [[EditDateController alloc] init];

            edc.managedTaskObject = self.managedTaskObject;
            edc.managedObjectContext = self.managedObjectContext;

            [self.navigationController pushViewController:edc animated:YES];
            [edc release];
            break;
        }
        case 3:
        {
            EditLocationController* elc = [[EditLocationController alloc] init];
            elc.managedObjectContext = self.managedObjectContext;
            elc.managedTaskObject = self.managedTaskObject;
[self.navigationController pushViewController:elc animated:YES];
            [elc release];
            break;
        }

        case 4:
        {

            UIAlertView* alert =
                [[[UIAlertView alloc] initWithTitle:@"Hi-Pri Tasks"
                                            message:nil
                                           delegate:self
                                  cancelButtonTitle:@"OK"
                                  otherButtonTitles:nil  ] autorelease];

            // Use Fetched property to get a list of high-pri tasks
            NSArray* highPriTasks = managedTaskObject.highPriTasks;
            NSMutableString* alertMessage =
                [[[NSMutableString alloc] init] autorelease];

            // Loop through each hi-pri task to create the string for
            // the message
            for (Task * theTask in highPriTasks)
            {
                [alertMessage appendString:theTask.text];
                [alertMessage appendString:@"
"];
            }

            alert.message = alertMessage;
            [alert show];

            break;
        }
        case 5:
        {

            UIAlertView* alert =
                [[[UIAlertView alloc] initWithTitle:@"Tasks due sooner"
                                            message:nil
                                           delegate:self
                                  cancelButtonTitle:@"OK"
                                  otherButtonTitles:nil  ] autorelease];
            NSMutableString* alertMessage =
                [[[NSMutableString alloc] init] autorelease];

            // need to get a handle to the managedObjectModel to use the stored
            // fetch request
            TasksAppDelegate* appDelegate =
                [UIApplication sharedApplication].delegate;
            NSManagedObjectModel* model = appDelegate.managedObjectModel;

            // Get the stored fetch request
            NSDictionary* dict =
                [[NSDictionary alloc]
initWithObjectsAndKeys:managedTaskObject.dueDate,
                 @"DUE_DATE",nil];

            NSFetchRequest* request =
                [model fetchRequestFromTemplateWithName:@"tasksDueSooner"
                                  substitutionVariables:dict];

            [dict release];

            NSError* error;
            NSArray* results =
                [managedObjectContext executeFetchRequest:request error:&error];

            // Loop through eachtask to create the string for the message
            for (Task * theTask in results)
            {
                [alertMessage appendString:theTask.text];
                [alertMessage appendString:@"
"];
            }

            alert.message = alertMessage;
            [alert show];

            break;
        }

        default:
            break;
    }
}
                                                         
Finishing Up the Editing Controllers

In the code for this method, cases 0 through 3 are very similar. Each creates an instance of the appropriate edit controller, populates the necessary properties, and then pushes the controller onto the navigation stack.

Case 4 implements the Hi-Pri Tasks feature, which uses the highPriTasks fetched property of the Task object that you defined in the previous chapter. If you don't remember, this property simply returns a list of all tasks that are marked as High Priority.

The interesting thing to notice about using the fetched property is that it returns an array of objects instead of a set. You can also see that using a fetched property is as simple as using a regular property. The code loops through each Task object returned from the fetched property and appends the Task name to a string. The code then displays the string using a UIAlertView.

Case 5 uses a stored fetch request to get a list of tasks that are due sooner than the current task. There are a couple of points of interest when using a stored fetch request. First, you need a reference to the managed object model because stored fetch requests reside in the model and not in your managed object class.

Next, if you specified substitution variables in the fetch request, you need to provide them to the fetch request in an NSDictionary that contains the objects and the keys. You can see that you are creating an NSDictionary using the dueDate property of the current Task object and the key text DUE_DATE. The key text is the same as the variable name that you specified in the previous chapter when defining the stored fetch request.

The code then creates an NSFetchRequest. It uses the fetchRequestFromTemplateWithName: method by supplying the name of the stored fetch request tasksDueSooner and the NSDictionary containing your substitution variables and keys.

The code then executes the fetch request against the context. Finally, the code iterates over the results, creating a string with the text from each returned Task object, and displays the string using a UIAlertView.

You are now ready to build and run the application. You should get a clean build with no errors or warnings. You should be able to add new tasks and edit all of the attributes of your tasks. You should also be able to create new locations and delete existing locations. Clicking the "hi-pri tasks" button in the task viewer will use the fetched property to display all of your high-priority tasks. The "Tasks due sooner" feature won't quite work yet because you have to implement date defaulting in the Task object. If you try to select "Tasks due sooner" and all of your tasks do not have due dates, you will get an error.

DISPLAYING RESULTS IN THE ROOTVIEWCONTROLLER

In this section, you are going to implement the filtering and sorting buttons on the RootViewController. The easiest way to implement this functionality is to modify the sort descriptor or predicate of the fetched results controller, and then execute the fetch.

Sorting Results with NSSortDescriptor

When the user taps the Asc or Dsc buttons, the toolbarSortOrderChanged method will run. In this method, you get a reference to the fetch request used by the fetched results controller and change the sort descriptor to match the sort order that the user selected. Then, you need to tell the fetchedResultsController to perform the fetch with the revised sort descriptor. Finally, you tell the TableView to reload its data. The following is the code for the toolbarSortOrderChanged method:

-(IBAction)toolbarSortOrderChanged:(id)sender;
{
    NSLog(@"toolbarSortOrderChanged");
    // Get the fetch request from the controller and change the sort descriptor
    NSFetchRequest* fetchRequest =  self.fetchedResultsController.fetchRequest;

    // Edit the sort key based on which button was pressed
    BOOL ascendingOrder = NO;
    UIBarButtonItem* button = (UIBarButtonItem*) sender;
    if ([button.title compare:@"Asc"]== NSOrderedSame)
        ascendingOrder=YES;
    else
        ascendingOrder=NO;
NSSortDescriptor *sortDescriptor =
        [[NSSortDescriptor alloc] initWithKey:@"text" ascending:ascendingOrder];
    NSArray *sortDescriptors =
        [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [sortDescriptor release];

    [fetchRequest setSortDescriptors:sortDescriptors];
    [sortDescriptors release];

    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    [taskTableView reloadData];
}
                                                         
Sorting Results with NSSortDescriptor

You use the same method regardless of which sort button the user tapped. At runtime, the code checks the title of the button to determine if the user wants the data sorted in ascending or descending order. Then, you use the compare method of the NSString class to perform the sort.

You can also add additional sort descriptors to sort your data using multiple fields. Core Data applies the sort descriptors to the results in the order that you specify them in the array that you pass into the setSortDescriptors function. You will learn more about implementing sort descriptors in the next chapter.

Filtering Results with NSPredicate

When the user taps the Hi-Pri button, your code should filter the list of tasks to show only high-priority tasks. Conversely, when the user selects the All button, you need to clear the filter to show all of the tasks again. You can build these features as you did with the sorting functionality in the last section. However, instead of modifying the sort descriptor, you will be modifying the fetch request's predicate. You can use predicates to filter data in all sorts of data structures. Their use is not limited to Core Data. You learn more about predicates in the next chapter.

The toolbarFilterHiPri method needs to set the predicate used by the fetch request to return tasks with a priority of 3. Then, the method has to tell the TableView to reload its data. The following is the code for the toolbarFilterHiPri method:

-(IBAction)toolbarFilterHiPri:(id)sender{
    NSLog(@"toolbarFilterHiPri");

    // Change the fetch request to display only high pri tasks
    // Get the fetch request from the controller and change the predicate
    NSFetchRequest* fetchRequest =  self.fetchedResultsController.fetchRequest;

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"priority == 3"];
    [fetchRequest setPredicate:predicate];
NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    [taskTableView reloadData];

}
                                                         
Filtering Results with NSPredicate

The code gets a reference to the predicate used by the fetchedResultsController. Then, you create a new predicate with the criteria of priority == 3. Next, you set the predicate of the fetch request to your new predicate, perform the fetch, and tell the table to reload its data.

The toolbarFilterAll method simply removes the predicate from the fetch request. You do that by setting the predicate to nil, as follows:

-(IBAction)toolbarFilterAll:(id)sender
{
    NSLog(@"toolbarFilterAll");

    // Change the fetch request to display all tasks
    // Get the fetch request from the controller and change the predicate
    NSFetchRequest* fetchRequest =  self.fetchedResultsController.fetchRequest;

    // nil out the predicate to clear it and show all objects again
    [fetchRequest setPredicate:nil];

    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    [taskTableView reloadData];
}
                                                         
Filtering Results with NSPredicate

This looks very similar to the toolbarFilterHiPri method except that here, you set the predicate to nil instead of creating a new predicate to apply to the fetchRequest. Removing the predicate effectively un-filters the data.

GENERATING GROUPED TABLES USING THE NSFETCHEDRESULTSCONTROLLER

In Chapter 3, you learned how to use the UILocalizedIndexedCollation class to create a TableView that organized data within sections. When you work with Core Data, you can use the NSFetchedResultsController to achieve the same results. In this section, you will build the LocationTasksViewController. The application displays the LocationTasksViewController when the user selects the Location button on the RootViewController. The LocationTasksViewController displays all of the tasks grouped by location.

The first step is to create a new UITableviewController without XIB called LocationTasksViewController. Modify the header file by adding instance variables and properties for the context and a fetched results controller. Also, mark your class as implementing the NSFetchedResultsControllerDelegate protocol. The header should look like Listing 7-7.

Example 7.7. LocationTasksViewController.h

#import <UIKit/UIKit.h>

@interface LocationTasksViewController :
    UITableViewController<NSFetchedResultsControllerDelegate> {

    NSManagedObjectContext *managedObjectContext;
    NSFetchedResultsController *fetchedResultsController;
}

@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain)
    NSFetchedResultsController *fetchedResultsController;
@end

Moving into the implementation file, add #import directives for the Location.h, Task.h, and ViewTaskController.h headers:

#import "Location.h"
#import "Task.h"
#import "ViewTaskController.h"
                                                         
LocationTasksViewController.h

Next, synthesize the properties that you declared in the header:

@synthesize managedObjectContext,fetchedResultsController;

Now, implement viewDidLoad to perform the fetch on the fetched results controller and set the title of the screen in the nav bar:

- (void)viewDidLoad {
    [super viewDidLoad];

    NSError* error;

    if (![[self fetchedResultsController] performFetch:&error]) {

        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
}

    // set the title to display in the nav bar
    self.title = @"Tasks by Location";
}
                                                         
LocationTasksViewController.h

In viewDidUnload, set the properties that you declared in the header to nil:

- (void)viewDidUnload {
    self.managedObjectContext=nil;
    self.fetchedResultsController = nil;
    [super viewDidUnload];
}
                                                         
LocationTasksViewController.h

Next, you will write the fetchedResultsController accessor method. You have implemented this method several times before. The difference in this case is that you will need to specify a sectionNameKeyPath when you initialize the NSFetchedResultsController.

The sectionNameKeyPath parameter allows you to specify a key path that the fetched results controller will use to generate the sections for your table. The fetched results controller will contain the entire set of Task objects. You want the tasks grouped by location. Remember that the Task object has a location property that refers to a related Location object. The Location object has a name property that contains the name of the Location. You really want the tasks grouped by the name property of the contents of the location property. Because you are holding a reference to a Task object, the key path to the Location's name property is location.name. The following is the code for the fetchedResultsController accessor:

- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    /*
     Set up the fetched results controller.
     */
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity =
        [NSEntityDescription entityForName:@"Task"
                    inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];

    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor =
        [[NSSortDescriptor alloc] initWithKey:@"location.name"
                                    ascending:YES];
NSArray *sortDescriptors =
        [[NSArray alloc] initWithObjects:sortDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController =
    [[NSFetchedResultsController alloc]
        initWithFetchRequest:fetchRequest
        managedObjectContext:managedObjectContext
          sectionNameKeyPath:@"location.name" cacheName:@"Task"];

    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    [aFetchedResultsController release];
    [fetchRequest release];
    [sortDescriptor release];
    [sortDescriptors release];

    return fetchedResultsController;
}
                                                         
LocationTasksViewController.h

If you look at the definition of the sort descriptor, you can see that you are also using the key path to the Location object's name property. In order for the fetched results controller to create the sections properly, you need to sort the result set using the same key that you use to generate the sections. As mentioned previously, the section key is set when initializing the NSFetchedResultsController in the sectionNameKeyPath parameter.

Next, you need to code the controllerDidChangeContent method to reload the data in the TableView when the contents of the fetched results controller changes:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // In the simplest, most efficient, case, reload the table view.
    [self.tableView reloadData];
}
                                                         
LocationTasksViewController.h

The next step is to implement the TableView methods. The numberOfSectionsInTableView method will look just like the corresponding method in the RootViewController. However, because you are using sections on this screen, the fetched results controller will not just return 1 for the number of sections. Instead, the fetched results controller will calculate the number of sections based on the number of different values in the location.name property.

The tableView:numberOfRowsInSection: method also uses the fetch results controller to populate the TableView. In this case, you get an NSFetchedResultsSectionInfo object from the fetched results controller that corresponds to the current section. The section info object has a numberOfObjects property that returns the number of objects in the section. This value is returned as the number of rows in the section.

Here are the implementations for the numberOfSectionsInTableView and tableView:numberOfRowsInSection: methods:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {

    id <NSFetchedResultsSectionInfo> sectionInfo =
        [[fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];

}
                                                         
LocationTasksViewController.h

The tableView:cellForRowAtIndexPath: and tableView:didSelectRowAtIndexPath: methods are the same as in the RootViewController. You generate the cell text the same way and the behavior when a user selects a row is the same. The following is the code for the tableView:cellForRowAtIndexPath: and tableView:didSelectRowAtIndexPath: methods:

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
    }

    // Configure the cell.
    Task *managedTaskObject =
        [fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = managedTaskObject.text;

    // Change the text color if the task is overdue
    if (managedTaskObject.isOverdue==[NSNumber numberWithBool: YES])
    {
        cell.textLabel.textColor = [UIColor redColor];
    }
    else {
        cell.textLabel.textColor = [UIColor blackColor];

    }
    return cell;
}
- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    //  Deselect the currently selected row according to the HIG
    [tableView deselectRowAtIndexPath:indexPath animated:NO];

    // Navigation logic may go here -- for example, create and push
    // another view controller.
    Task *managedObject =
        [fetchedResultsController objectAtIndexPath:indexPath];

    ViewTaskController* taskController =
        [[ViewTaskController alloc]
         initWithStyle:UITableViewStyleGrouped];

    taskController.managedTaskObject=managedObject;
    taskController.managedObjectContext = self.managedObjectContext;

    [self.navigationController pushViewController:taskController animated:YES];

    [taskController release];

}
                                                         
LocationTasksViewController.h

There is one additional method that you need to implement tableView:titleForHeaderInSection:. This method generates the titles for the sections of the table. Again, you will use an NSFetchedResultsController to get these titles. The fetched results controller maintains a sections array that contains the list of sections in the result set. You simply need to get the item out of the array that corresponds to the section that the TableView is asking for. Here is the code:

- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> sectionInfo =
        [[fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo name];
}
                                                         
LocationTasksViewController.h

Finally, modify the dealloc method to release your instance variables:

- (void)dealloc {
    [fetchedResultsController release];
    [managedObjectContext release];
    [super dealloc];
}
                                                         
LocationTasksViewController.h

Now that you have built the LocationTasksViewController, you need to modify the RootViewController to call your new location controller when the user taps the Location button. In the RootViewController.m implementation file, add a #import directive to import the new controller:

#import "LocationTasksViewController.h"

Modify the code in the locationButtonPressed method to create an instance of your new controller and push it on to the navigation stack:

-(IBAction)locationButtonPressed:(id)sender
{
    NSLog(@"locationButtonPressed");

    LocationTasksViewController* ltvc =
        [[LocationTasksViewController alloc]
            initWithStyle:UITableViewStylePlain];
    ltvc.managedObjectContext = self.managedObjectContext;
    [self.navigationController pushViewController:ltvc animated:YES];
    [ltvc release];
}
                                                         
LocationTasksViewController.h

You are now ready to build and run the application. You should be able to use all of the buttons at the bottom of the RootViewController to filter the data, sort the data, and bring up the tasks grouped by location screen.

IMPLEMENTING CUSTOM MANAGED OBJECTS

Up until this point, you haven't used any of the features of an NSManagedObject subclass. You could have written all of the Core Data code that you have seen so far in this chapter using key-value coding and generic NSManagedObjects.

In this section, you learn about the additional features that you can implement by using custom subclasses of NSManagedObject. These features include dynamically calculating property values at runtime, defaulting data at runtime, and single and multiple field validation.

Coding a Dynamic Property

You can use dynamic properties to implement properties that you need to calculate at runtime. In the data modeler, you should mark dynamic properties as Transient because you will compute their value at runtime and will not save this value in the data store.

In this example, you will code an isOverdue property for the Task object. This property should return YES if a task is overdue and NO if it is not. You may notice that when you declare a Boolean property in the data modeler, Xcode translates the property into an NSNumber in the generated code. This is not a problem because there is a class method in the NSNumber class that returns an NSNumber representation of a BOOL.

The following is the code for the isOverdue accessor method:

- (NSNumber*) isOverdue
{
    BOOL isTaskOverdue = NO;

    NSDate* today = [NSDate date];

    if (self.dueDate != nil) {
        if ([self.dueDate compare:today] == NSOrderedAscending)
            isTaskOverdue=YES;
    }

    return [NSNumber numberWithBool:isTaskOverdue];
}
                                                         
Coding a Dynamic Property

This code simply compares the dueDate in the current Task object against the current system date. If the dueDate is earlier than today, the code sets the isTaskOverdue variable to YES. The code then uses the numberWithBool method to return an NSNumber that corresponds to the Boolean result.

If you run the application now, any tasks that occurred in the past should turn red in the RootViewController.

Defaulting Data at Runtime

Just as you can dynamically generate property values at runtime, you can also generate default values at runtime. There is a method called awakeFromInsert that the Core Data framework calls the first time that you insert an object into the managed object context.

Subclasses of NSManagedObject can implement awakeFromInsert to set default values for properties. It is critical that you call the superclass implementation of awakeFromInsert before doing any of your own implementation.

Another consideration is that you should always use primitive accessor methods to modify property values while you are in the awakeFromInsert method. Primitive accessor methods are methods of the Managed Objects that are used to set property values but that do not notify other classes observing your class using key-value observing. This is important because if you modify a property and your class sends out a notification that results in another class modifying the same property, your code could get into in an endless loop.

You can set primitive values in your code using key value coding and the setPrimitiveValue:forKey: method. A better way is to define custom primitive accessors for any properties that you need to access in this way. In this case, you will be modifying the dueDate property in awakeFromInsert so you need to define a primitive accessor for the dueDate. Fortunately, Core Data defines these properties for you automatically at runtime. All that you need to do is declare the property in the Task header:

@property (nonatomic, retain) NSDate * primitiveDueDate;

Then, add an @dynamic directive in the implementation file to indicate that Core Data will dynamically generate this property at runtime:

@dynamic primitiveDueDate;

Now, you can set the dueDate property in the awakeFromInsert method without fear of any side effects that may occur because of the possibility that other classes are observing the Task for changes.

Implement the awakeFromInsert method to create an NSDate object three days from the current date and use the primitive property to set the default date. The following is the code for the awakeFromInsert method:

- (void)awakeFromInsert
{
    // Core Data  calls this function the first time the receiver
    // is inserted into a context.
    [super awakeFromInsert];

    // Set the due date to 3 days from now (in seconds)
    NSDate* defualtDate = [[NSDate alloc]
                           initWithTimeIntervalSinceNow:60*60*24*3];

    // Use custom primitive accessor to set dueDate field
    self.primitiveDueDate = defualtDate ;

    [defualtDate release];
}
                                                         
Defaulting Data at Runtime

Before you build and run the application, delete the old app from the simulator first because you may have tasks that do not have due dates. Now you can run the application. New tasks that you create will now have a default due date set three days into the future. You should be able to use the "Tasks due sooner than this one" button on the task detail screen now because all tasks will have defaulted due dates.

Validating a Single Field

Single-field validation in a custom class is straightforward. Core Data will automatically call a method called validateXxxx if you have implemented the method. The Xxxx is the name of your property with the first letter capitalized. The method signature for the validate method for the dueDate field looks like this:

-(BOOL)validateDueDate:(id *)ioValue error:(NSError **)outError{

A single field validation method should return YES if the validation is successful and NO if it failed. The method accepts an id*, which is the value that you are testing for validity and an NSError** that you should use to return an NSError object if the validation fails.

Because this method receives an id*, you could modify the object that is passed into the validation function, but you should never do this. Users of a class would not expect validation to modify the object that was submitted for validation. Modifying the object that was passed in would create an unexpected side effect. Creating side effects is usually a poor design choice which you should avoid. You should treat the object passed in for validation as read only.

In the validation of the dueDate of the Task object, you are going to enforce a rule that assigning due dates that have occurred in the past is invalid. Here is the implementation of the dueDate validation function:

-(BOOL)validateDueDate:(id *)ioValue error:(NSError **)outError{

    // Due dates in the past are not valid.
    // enforce that a due date has to be >= today's date
    if ([*ioValue compare:[NSDate date]] == NSOrderedAscending) {

        if (outError != NULL) {
            NSString *errorStr = [[[NSString alloc] initWithString:
                                   @"Due date must be today or later"]
                                  autorelease];
            NSDictionary *userInfoDictionary =
                [NSDictionary dictionaryWithObject:errorStr
                                            forKey:@"ErrorString"];
            NSError *error =
                [[[NSError alloc] initWithDomain:TASKS_ERROR_DOMAIN
                                  code:DUEDATE_VALIDATION_ERROR_CODE
                                  userInfo:userInfoDictionary] autorelease];
            *outError = error;
        }
        return NO;
    }
    else {
        return YES;
    }
}
                                                         
Validating a Single Field

The first thing that you do is check the date that you receive as an input parameter and compare it to the current system date. If the comparison fails, you create an error string that you will return to the caller in the NSError object. Next, you add the error string to an NSDictionary object that you pass back to the class user as the userInfo in the NSError object. Then, you allocate and initialize an NSError object with an error domain and error code. The domain and code are custom values used to identify your error and can be any values that you like. For this sample, I have defined them in the Task.h header file like this:

#define TASKS_ERROR_DOMAIN                      @"com.Wrox.Tasks"
#define DUEDATE_VALIDATION_ERROR_CODE           1001
                                                         
Validating a Single Field

You pass the userInfo dictionary that you created to hold the error string to the initializer of the NSError object. Users of your Task class can interrogate the userInfo dictionary that they receive in the NSError to get details about the problem and act accordingly. Finally, you return NO to indicate that validation has failed.

If the validation succeeds, the code simply returns YES.

Run the application now and try to set the due date for a task to a date in the past. You should get an error indicating that this is invalid.

Multi-Field Validation

Multi-field validation is slightly more complicated than single-field validation. Core Data will call two methods automatically if they exist: validateForInsert and validateForUpdate. Core Data calls validateForInsert when you insert an object into the context for the first time. When you update an existing object, Core Data calls validateForUpdate.

If you want to implement a validation rule that runs both when an object is inserted or updated, I recommend writing a new validation function and then calling that new function from both the validateForInsert and validateForUpdate methods. The example follows this approach.

In this sample, you will be enforcing a multi-field validation rule that says that high-priority tasks must have a due date within the next three days. Any due date farther in the future should cause an error. You cannot accomplish this within a single field validation rule because you need to validate both that the task is high priority and that the due date is within a certain range. In the Task.h header file, add a new method declaration for the function that you will call to validate the data:

- (BOOL)validateAllData:(NSError **)error;

Here is the function that enforces the rule:

- (BOOL)validateAllData:(NSError **)outError
{
    NSDate* compareDate =
        [[[NSDate alloc] initWithTimeIntervalSinceNow:60*60*24*3] autorelease];
    // Due dates for hi-pri tasks must be today, tomorrow, or the next day.
    if ([self.dueDate compare:compareDate] == NSOrderedDescending &&
        [self.priority intValue]==3) {

        if (outError != NULL) {
            NSString *errorStr = [[[NSString alloc] initWithString:
                 @"Hi-pri tasks must have a due date within two days of today"]
                                  autorelease];
            NSDictionary *userInfoDictionary =
                [NSDictionary dictionaryWithObject:errorStr
                                            forKey:@"ErrorString"];
            NSError *error =
                [[[NSError alloc] initWithDomain:TASKS_ERROR_DOMAIN
                                  code:PRIORITY_DUEDATE_VALIDATION_ERROR_CODE
                                  userInfo:userInfoDictionary] autorelease];
            *outError = error;
        }
return NO;
    }
    else {
        return YES;
    }
}
                                                         
Multi-Field Validation

The code first generates a date to which to compare the selected dueDate. Then, the code checks to see if the dueDate chosen is greater than this compare date and that the priority of the task is high. If the data meets both of these criteria, it is invalid and you generate an NSError object just like in the last section. A new error code is used and should be added to the Task.h header:

#define PRIORITY_DUEDATE_VALIDATION_ERROR_CODE  1002

The code then returns NO to indicate that the validation has failed. If the validation is successful, the method returns YES.

Now, you have to add the two validation methods that Core Data calls and code them to call your validateAllData method:

- (BOOL)validateForInsert:(NSError **)outError
{
    // Call the superclass validateForInsert first
    if ([super validateForInsert:outError]==NO)
    {
        return NO;
    }

    // Call out validation function
    if ([self validateAllData:outError] == NO)
    {
        return NO;
    }
    else {
        return YES;
    }
}

- (BOOL)validateForUpdate:(NSError **)outError
{
    // Call the superclass validateForUpdate first
    if ([super validateForUpdate:outError]==NO)
    {
        return NO;
    }

    // Call out validation function
    if ([self validateAllData:outError] == NO)
    {
        return NO;
}
    else {
        return YES;
    }
}
                                                         
Multi-Field Validation

First, both of these methods call their superclass counterpart method. You have to call the superclass method because that method handles validation rules implemented in the model and calls the single-field validation methods. If the superclass validation routine is successful, the methods go on to call your validateAllData method.

Build and run the application. If you try to set the due date for a high-priority task to more than two days in the future, or if you try to set the priority of a task to high that has a due date more than two days in the future, you will get an error.

MOVING FORWARD

This chapter covered a lot of material. You learned how to implement the Core Data concepts that you learned about in the last chapter.

Now you have a fully functioning Core Data–based application that demonstrates many of the features of Core Data. You can use this application like a sandbox to play with these features or implement new functionality in the Tasks application on your own.

Now that you have built an entire application, I hope that you have confidence in your ability to implement an application using the Core Data framework. You should now be able to use Core Data effectively in your own applications.

In the next chapter, you will learn more about some features of Cocoa Touch that you often use with Core Data. You will explore key value coding and key value observing. You will also learn more about NSPredicates and how to implement sort descriptors using your own custom classes. Finally, you will learn how to migrate your existing data from one Core Data model version to another.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset