Table Views - Learning Core Data for iOS (2014)

Learning Core Data for iOS (2014)

5. Table Views

If we knew what it was we were doing, it would not be called research, would it?

Albert Einstein

In Chapter 4, “Managed Object Model Expansion,” you tapped into the flexibility that relationships and entity inheritance add to a managed object model. Up until now, the demonstrations were constrained to the console log. It’s now time to move closer to the end user experience as you’re shown how to efficiently present Core Data–fetched results in a table view. This chapter will begin with a brief refresher on table views and then dive right in to constructing a Core Data–driven Table View Controller subclass. This reusable subclass will be leveraged to populate two new table views—one for preparing a shopping list and one for shopping with.

Table Views 101

Arguably one of the most common iOS interface elements is the table view. Based on UIScrollView, this powerful part of UIKit offers a highly customizable way to display a list of information in a single column. Even if you’re new to Core Data, you may already be familiar with creating table views populated using an NSArray. In case you’re unfamiliar, this section will outline the basics of table views.

Figure 5.1 shows the main components of a table view, in which the section header title and table view cell (row) locations should be apparent. The section index is the small, vertical text shown down the right side. Tapping or dragging the section index allows you to jump quickly between sections within the table view.

Image

Figure 5.1 Fundamental table view components

To populate a table view, you would usually create a UITableViewController subclass adopting the UITableViewDataSource protocol. You would then assign the subclass to a Table View Controller on a storyboard. The UITableViewDataSource protocol requires that a couple of mandatory methods be implemented to allow the table to be populated with data:

image numberOfRowsInSection is where you would specify how many rows each table view section has. For example, you might configure this method to return [someArray count], so the number of objects in the data source array matches the number of rows in the table view.

image cellForRowAtIndexPath is where you would specify what will be displayed in each cell. It’s common to heavily customize this method. If you use the built-in cell styles, there are some default properties available with a standard UITableViewCell to be aware of. For example, the text shown in each row of Figure 5.1 appears because the textLabel.text property has been set. For a complete list of properties, you can jump to the definition of UITableViewCell in Xcode.

Other optional methods are available by adopting the UITableViewDataSource protocol. These methods may be used to configure editing, reordering, deleting, headers, footers, the index, and more. Most of them will be covered later in this chapter.

Core Data Table Views

As previously mentioned, if you weren’t using Core Data, you might populate a table view using an NSArray as the data source. A key problem with this approach, which you may have experienced first hand, is that performance can suffer when the array is too big and consumes too much memory. Up until now, you’ve performed Core Data fetches using an NSFetchRequest, which produces an NSArray. Although you could use this array directly to populate a table view, there is a better way. To populate a table view, you’ll still fetch with an NSFetchRequest; however, this time you’ll configure additional options such as setFetchBatchSize to stagger the fetch. This small option can have a huge impact on the memory footprint and consequently improve overall performance. The batch size you set should be a bit larger than the number of rows visible on the screen at any one time.

The best way to efficiently manage fetched data between Core Data and a table view is with an NSFetchedResultsController. If you were to otherwise use the array returned by a fetch request directly without a fetched results controller, there’s a chance that when the underlying data changes, the objects in the array could become invalid. This could lead to a crash.

Setting a table view as a delegate of a fetched results controller enables change tracking, which will help update the table view automatically when fetched objects change in the underlying context. The performance of a fetched results controller–backed table view can also be increased by setting a cache, which is as easy as specifying a unique name for the cache. Using a cache will minimize unnecessary repeated fetches. As well as the performance and change-tracking benefits, a fetched results controller has a number of convenient properties that make it trivial to wire up a Core Data table view.


Note

To continue building the sample application, you’ll need to have added the previous chapter’s code to Grocery Dude. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LearningCoreData/GroceryDude-AfterChapter04.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to click Product > Clean. This practice ensures there’s no residual cache from previous projects using the same name. Also, delete any existing copy of Grocery Dude from your device or the iOS Simulator.


Introducing CoreDataTVC

CoreDataTVC will be a reusable subclass that underpins all Core Data table views in Grocery Dude. It will also be generic enough to reuse in your own applications. To create CoreDataTVC, you’ll create a UITableViewController subclass with an NSFetchedResultsController instance variable.

Update Grocery Dude as follows to create CoreDataTVC:

1. Select the Generic Core Data Classes group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to UITableViewController and Class name to CoreDataTVC and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

Figure 5.2 shows the expected results.

Image

Figure 5.2 CoreDataTVC

Select CoreDataTVC.m and then click inside the class editor window. Xcode will warn that CoreDataTVC.m isn’t configured properly. You may safely ignore these warnings for the moment.


Note

The CoreDataTVC class name was chosen against CoreDataTableViewController to minimize the amount of repeated text throughout this book. This goes against standard Objective-C naming conventions and would otherwise be discouraged. Class names should be expressive, clear, and unambiguous. For example, TVC could be confused for Table View Cell instead of Table View Controller.


The CoreDataTVC class will simply be a UITableViewController subclass that adopts the NSFetchedResultsControllerDelegate protocol and contains a property for an NSFetchedResultsController named frc. As shown in Listing 5.1, it will also have a method named performFetch. This method will be responsible for fetching data and refreshing the table view, with some error reporting just in case the fetch fails.

Listing 5.1 CoreDataTVC.h


#import <UIKit/UIKit.h>
#import "CoreDataHelper.h"
@interface CoreDataTVC : UITableViewController
<NSFetchedResultsControllerDelegate>
@property (strong, nonatomic) NSFetchedResultsController *frc;
- (void)performFetch;
@end


Update Grocery Dude as follows to configure the CoreDataTVC header:

1. Replace all code in CoreDataTVC.h with the code from Listing 5.1. Continue to ignore the Xcode warnings.

The CoreDataTVC class implementation has three main sections. For easy navigation and readability, they’re separated by pragma marks.

image FETCHING

image DATASOURCE: UITableView

image DELEGATE: NSFetchedResultsController

Fetching

As shown in Listing 5.2, the FETCHING section of CoreDataTVC.m implements the performFetch method. As previously mentioned, this method is responsible for fetching data and refreshing the table view. If errors occur during the fetch, they’ll be logged to the console.

Listing 5.2 CoreDataTVC.m: FETCHING


#import "CoreDataTVC.h"
@implementation CoreDataTVC
#define debug 1

#pragma mark - FETCHING
- (void)performFetch {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}

if (self.frc) {
[self.frc.managedObjectContext performBlockAndWait:^{

NSError *error = nil;
if (![self.frc performFetch:&error]) {

NSLog(@"Failed to perform fetch: %@", error);
}
[self.tableView reloadData];
}];
} else {
NSLog(@"Failed to fetch, the fetched results controller is nil.");
}
}
@end


Update Grocery Dude as follows to configure the CoreDataTVC implementation:

1. Replace all code in CoreDataTVC.m with the code from Listing 5.2. The warnings should now have disappeared.

DATASOURCE: UITableView

CoreDataTVC inherits from UITableViewController, so it adopts the UITableViewDataSource protocol by default. By adopting this protocol, CoreDataTVC or one of its subclasses is responsible for implementing the previously mentioned mandatory methods numberOfRowsInSection andcellForRowAtIndexPath. Without these methods, the table view would remain empty with no visible data. The cellForRowAtIndexPath method will be implemented later within a subclass of CoreDataTVC because it will always be unique.

There are nine optional UITableViewDataSource protocol methods; four of them are generic enough to be implemented in CoreDataTVC. A fetched results controller can easily provide the values they need to return.

image numberOfSectionsInTableView indicates how many sections the table view has. The default value is 1 if this method isn’t implemented. When an instance of a fetched results controller is created, you have an opportunity to configure a sectionNameKeyPath, which organizes results into sections. To handle all cases of fetched result sections appropriately, you simply need to configure this method to return [[self.frc sections] count]. This will ensure that when multiple sections are required by the fetched results controller, that the Table View Controller can automatically handle it.

image sectionForSectionIndexTitle indicates what section a particular section title belongs to. A fetched results controller has a method specifically to help populate this table view data source method. This means returning [self.frc sectionForSectionIndexTitle:title atIndex:index] is all that’s required here.

image titleForHeaderInSection indicates what text title should be shown in a particular section. Generally, returning [[[self.frc sections] objectAtIndex:section] name] will suffice to provide appropriate section information with respect to any sectionNameKeyPath you may have configured.

image sectionIndexTitlesForTableView indicates the text title of each index that should be shown in the table view. A fetched results controller has a property specifically to help populate this table view data source method. This means returning [self.frc sectionIndexTitles] is all that’s required here.

As shown in Listing 5.3, the code to populate a table view using the UITableViewDataSource protocol methods is trivial. You may wish to override methods such as the header title later on in a CoreDataTVC subclass; however, this is a great starting point for now.

Listing 5.3 CoreDataTVC.m: DATASOURCE: UITableView


#pragma mark - DATASOURCE: UITableView
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [[self.frc.sections objectAtIndex:section] numberOfObjects];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [[self.frc sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView
sectionForSectionIndexTitle:(NSString *)title
atIndex:(NSInteger)index {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [self.frc sectionForSectionIndexTitle:title atIndex:index];
}
- (NSString *)tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [[[self.frc sections] objectAtIndex:section] name];
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return [self.frc sectionIndexTitles];
}


Update Grocery Dude as follows to update the CoreDataTVC implementation:

1. Add the code from Listing 5.3 to the bottom of CoreDataTVC.m before @end.

DELEGATE: NSFetchedResultsController

You can tell by looking in its header file that CoreDataTVC adopts the NSFetchedResultsControllerDelegate protocol, which means optional methods can now be implemented to ensure the Table View Controller correctly handles moves, deletes, updates, and insertions. Whenever you need to make a change to a table view, you need to tell it to beginUpdates, and when you’re done, endUpdates. When you’re using a fetched results controller, you need to call these methods from controllerWillChangeContent and controllerDidChangeContent, respectively, as shown in Listing 5.4.

Listing 5.4 CoreDataTVC.m Content Changes


#pragma mark - DELEGATE: NSFetchedResultsController
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
[self.tableView endUpdates];
}


Update Grocery Dude as follows to update the CoreDataTVC implementation:

1. Add the code from Listing 5.4 to the bottom of CoreDataTVC.m before @end.

The next two fetched results controller delegate protocol methods handle moves, deletes, updates, and insertions, depending on the given change type. Listing 5.5 shows the code involved. Note that NSFetchedResultsChangeUpdate in the didChangeObject method has no row animation as opposed to other change types in the same method. This speeds up user interaction with table view cells. In the case of Grocery Dude, when someone ticks an item off the shopping list, the tick should appear immediately. The chosen animation option makes sure there’s no fade-in delay. You may wish to set this to UITableViewRowAnimationAutomatic if you adapt this code to your own projects.

Listing 5.5 CoreDataTVC.m: DELEGATE: NSFetchedResultsController


- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type {

if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {

if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
if (!newIndexPath) {
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
} else {
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}


Update Grocery Dude as follows to update the CoreDataTVC implementation:

1. Add the code from Listing 5.5 to the bottom of CoreDataTVC.m before @end.

CoreDataTVC is now ready to be subclassed. There will be five table views in Grocery Dude. This means there will also be five separate CoreDataTVC subclasses, one for each customized table view, all with similar code:

image PrepareTVC will list items that can be put on the shopping list.

image ShopTVC will list items that are on the shopping list.

image UnitsTVC will list units that items are measured by (for example, Kg, g, or liters). This class will be implemented in the next chapter.

image LocationsAtHomeTVC will list the possible locations items can be stored in at home. This class will be implemented in the next chapter.

image LocationsAtShopTVC will list the possible aisle locations items can be in at a shop. This class will be implemented in the next chapter.

The PrepareTVC and ShopTVC table views will be Grocery Dude’s primary views. The application will need a Tab Bar Controller to allow switching between them.

Update Grocery Dude as follows to add a Tab Bar Controller:

1. Select Main.storyboard.

2. Drag a Tab Bar Controller onto the storyboard to the left of the existing Navigation Controller.

3. Delete the two default view controllers connected to the Tab Bar Controller.

4. Hold down Control and drag a line from the Tab Bar Controller to the existing Navigation Controller and then select Relationship Segue > view controllers.

5. Set the Tab Bar Controller as the initial view controller using Attributes Inspector (Option+image+4), as shown in Figure 5.3.

Image

Figure 5.3 A Tab Bar Controller will be used to toggle table views.

A Tab Bar Controller isn’t any good with just one tab, so another needs to be added for the upcoming ShopTVC table view. Before that can happen, the ShopTVC table view itself needs to be created.

Update Grocery Dude as follows to add a new table view connected to the tab bar:

1. Select Main.storyboard.

2. Drag a new Table View Controller onto the storyboard beneath the existing Navigation Controller.

3. Ensure that the new Table View Controller is selected and then click Editor > Embed In > Navigation Controller.

4. Set the Navigation Item Title of the new Table View Controller to Grocery Dude using Attributes Inspector (Option+image+4).

5. Click the Prototype Table View Cell of the new Table View Controller and set the Reuse Identifier to Shop Cell. This identifier will be referred to in ShopTVC’s cellForRowAtIndexPath method as it populates these cells with data.

6. Hold down Control and drag a line from the Tab Bar Controller to the new Navigation Controller; then select Relationship Segue > View controllers.

7. Vertically center the Tab Bar Controller so it lines up nicely, as shown in Figure 5.4.

Image

Figure 5.4 The Tab Bar Controller now has two tabs.

The Tab Bar Controller now has two tabs that will be used to cycle between the PrepareTVC and ShopTVC table views. Before those subclasses are created and assigned to the table views, final touches are needed to help identify the tabs properly.

Update Grocery Dude as follows to add tab bar icons:

1. Download and extract the tab bar icons from the following URL: http://www.timroadley.com/LearningCoreData/TabBarIcons.zip.

2. Select the Images.xcassets asset catalog.

3. Drag the tab bar icons into the asset catalog beneath LaunchImage, as shown in Figure 5.5.

Image

Figure 5.5 The Tab Bar Controller icons

Update Grocery Dude as follows to configure the tabs:

1. Select Main.storyboard.

2. Select the Tab Bar Item on the Navigation Controller next to the Items table view.

3. Set the Bar Item Title to Prepare and Bar Item Image to prepare, as shown in Figure 5.6.

Image

Figure 5.6 The Prepare tab

4. Select the Tab Bar Item on the Navigation Controller next to the Grocery Dude table view.

5. Set the Bar Item Title to Shop and Bar Item Image to shop using the technique from step 3.

6. Run the application. You should be able to switch between table views, as shown in Figure 5.7. There won’t be any data in the tables just yet. If the Prepare and Shop tabs are in the wrong order, you can drag them to the correct order using the Tab Bar Controller on the storyboard.

Image

Figure 5.7 Grocery Dude’s Tab Bar Controller allows table view toggling

AppDelegate’s CoreDataHelper Instance

Frequently throughout the application, access will be required to the shared instance of CoreDataHelper. This means that the existing cdh code found in AppDelegate.m needs to be exposed through AppDelegate.h. Listing 5.6 shows the code required to expose cdh.

Listing 5.6 AppDelegate.h: cdh


- (CoreDataHelper*)cdh;


Update Grocery Dude as follows to expose the cdh method:

1. Add the code from Listing 5.6 to the bottom of AppDelegate.h before @end.

The existing implementation of cdh is intended to return the shared instance of CoreDataHelper. At present, this method isn’t thread-safe because there’s no guarantee that CoreDataHelper won’t be instantiated more than once from separate threads. An updated version of cdh is shown in Listing 5.7. This updated method wraps the instantiation of CoreDataHelper in dispatch_once to resolve the thread safety issue. It does this by guaranteeing that CoreDataHelper can only be instantiated once for the lifetime of the application.

Listing 5.7 AppDelegate.m: cdh


- (CoreDataHelper*)cdh {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
if (!_coreDataHelper) {
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
_coreDataHelper = [CoreDataHelper new];
});
[_coreDataHelper setupCoreData];
}
return _coreDataHelper;
}


Update Grocery Dude as follows to implement a thread-safe cdh method:

1. Replace the existing cdh method in AppDelegate.m with the code from Listing 5.7.

Introducing PrepareTVC

To provide data to the table view shown on the Prepare tab, a new class named PrepareTVC will be created as a subclass of CoreDataTVC. This subclass will configure a fetch request and display items that can be put on a shopping list.

Update Grocery Dude as follows to add PrepareTVC:

1. Right-click the existing Grocery Dude group and create a new group called Grocery Dude Table View Controllers.

2. Ensure the new Grocery Dude Table View Controllers group is selected.

3. Click File > New > File....

4. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

5. Set Subclass of to CoreDataTVC and Class name to PrepareTVC. Click Next.

6. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

There isn’t much to the PrepareTVC header. As shown in Listing 5.8, the only thing worth noting is that this class adopts the UIActionSheetDelegate protocol. In addition, there’s an instance variable that will hold an action sheet. To put an item on the shopping list (the Shop tab), a user will simply tap an item on the Prepare tab. To completely clear the shopping list, a Clear button will be implemented on the Prepare tab. To prevent the shopping list from being accidentally cleared, an action sheet is needed to confirm that a shopping list should be cleared. This is the sole purpose of the clearConfirmActionSheet property shown in Listing 5.8.

Listing 5.8 PrepareTVC.h


#import <UIKit/UIKit.h>
#import "CoreDataTVC.h"
@interface PrepareTVC : CoreDataTVC <UIActionSheetDelegate>
@property (strong, nonatomic) UIActionSheet *clearConfirmActionSheet;
@end


Update Grocery Dude as follows to configure PrepareTVC:

1. Replace all code in PrepareTVC.h with the code from Listing 5.8.

The implementation files of the upcoming subclasses of CoreDataTVC will all have sections for DATA, VIEW, and INTERACTION.

Data

The DATA section of the PrepareTVC implementation contains only a configureFetch method. This method creates a fetched results controller based on a customized NSFetchRequest. It also sets PrepareTVC as a delegate of the fetched results controller. The delegate methods are already implemented in the superclass CoreDataTVC, so you don’t have to worry about repeating that code. The code involved is shown in Listing 5.9.

Listing 5.9 PrepareTVC.m: DATA


#import "PrepareTVC.h"
#import "CoreDataHelper.h"
#import "Item.h"
#import "Unit.h"
#import "AppDelegate.h"

@implementation PrepareTVC
#define debug 1

#pragma mark - DATA
- (void)configureFetch {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];

NSFetchRequest *request =
[NSFetchRequest fetchRequestWithEntityName:@"Item"];

request.sortDescriptors =
[NSArray arrayWithObjects:
[NSSortDescriptor sortDescriptorWithKey:@"locationAtHome.storedIn"
ascending:YES],
[NSSortDescriptor sortDescriptorWithKey:@"name"
ascending:YES],
nil];
[request setFetchBatchSize:50];
self.frc =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:cdh.context
sectionNameKeyPath:@"locationAtHome.storedIn"
cacheName:nil];
self.frc.delegate = self;
}
@end


Most of the code in configureFetch should be familiar because it primarily involves the creation of an NSFetchRequest, as discussed in Chapter 2, “Managed Object Model Basics.” The remaining code staggers the fetch into batches with setFetchBatchSize and also configures self.frc with an instance of NSFetchedResultsController. To create the fetched results controller, you need four things:

image An instance of NSFetchRequest. In this case, request was created at the start of the configureFetch method using techniques from previous chapters.

image An instance of NSManagedObjectContext. In this case, the cdh convenience method of the AppDelegate is leveraged to provide this.

image A string representing a sectionNameKeyPath. This string value represents an entity attribute key and is used to group the table view into sections. In this case, locationAtHome.storedIn indicates the table view should be grouped into sections representing the location where items are stored in the user’s home. It’s important to note that the attribute specified here must also be the first sort descriptor in the fetch request.

image A string representing a cache. Although it’s not provided in this case, if it were it should be a string that’s unique across the entire application.

Update Grocery Dude as follows to implement the DATA section in PrepareTVC:

1. Replace all code in PrepareTVC.m with the code from Listing 5.9.

View

The VIEW section of PrepareTVC.m is where most of the action happens. This section mostly consists of table view data source methods and a viewDidLoad method. The code involved is shown in Listing 5.10.

Listing 5.10 PrepareTVC.m: VIEW


#pragma mark - VIEW
- (void)viewDidLoad {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
[super viewDidLoad];
[self configureFetch];
[self performFetch];
self.clearConfirmActionSheet.delegate = self;

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(performFetch)
name:@"SomethingChanged"
object:nil];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
static NSString *cellIdentifier = @"Item Cell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryDetailButton;
Item *item = [self.frc objectAtIndexPath:indexPath];
NSMutableString *title = [NSMutableString stringWithFormat:@"%@%@ %@",
item.quantity, item.unit.name, item.name];
[title replaceOccurrencesOfString:@"(null)"
withString:@""
options:0
range:NSMakeRange(0, [title length])];
cell.textLabel.text = title;

// make selected items orange
if ([item.listed boolValue]) {
[cell.textLabel setFont:[UIFont
fontWithName:@"Helvetica Neue" size:18]];
[cell.textLabel setTextColor:[UIColor orangeColor]];
}
else {
[cell.textLabel setFont:[UIFont
fontWithName:@"Helvetica Neue" size:16]];
[cell.textLabel setTextColor:[UIColor grayColor]];
}
return cell;
}
- (NSArray*)sectionIndexTitlesForTableView:(UITableView *)tableView {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return nil; // we don't want a section index.
}


The viewDidLoad method is responsible for configuring and performing the fetch that drives the table view. It also configures the table view as an action sheet delegate, which will be used later to verify that a user wants to clear the entire shopping list. The final code in this method configures the table view to listen for a SomethingChanged notification. This enables other areas of the application to trigger a re-fetch if required (for example, when the Core Data Stack is completely reset).

The cellForRowAtIndexPath method is responsible for wiring up the data that is displayed in each table view cell. Table view cell creation is optimized using dequeueReusableCellWithIdentifier. The method continues on to color-code items depending on whether or not they’re listed on the Shop tab. There is also logic involved in removing ugly (null) values encountered when a user doesn’t enter an item name.

The sectionIndexTitlesForTableView method is overridden from CoreDataTVC to return nil. Returning nil disables the index. If you don’t override this method, the superclass implementation enables the section index.

Update Grocery Dude as follows to implement the VIEW section:

1. Add the code from Listing 5.10 to the bottom of PrepareTVC.m before @end.

2. Select Main.storyboard.

3. Select the Items Table View Controller.

4. Set the Custom Class of the Items Table View Controller to PrepareTVC using Identity Inspector (Option+image+3), as shown in Figure 5.8.

Image

Figure 5.8 PrepareTVC Table View Controller

The PrepareTVC is now in a position to display data, yet there’s nothing in the persistent store to show because the application should have been deleted at the beginning of this chapter.

Update Grocery Dude as follows to insert some test objects:

1. Add #import "LocationAtHome.h" to the top of AppDelegate.m.

2. Add #import "LocationAtShop.h" to the top of AppDelegate.m.

3. Replace all code in the demo method of AppDelegate.m with the code shown in Listing 5.11. This code inserts test data using techniques discussed in the previous chapters.

Listing 5.11 AppDelegate.m: demo


CoreDataHelper *cdh = [self cdh];
NSArray *homeLocations = [NSArray arrayWithObjects:
@"Fruit Bowl",@"Pantry",@"Nursery",@"Bathroom",@"Fridge",nil];
NSArray *shopLocations = [NSArray arrayWithObjects:
@"Produce",@"Aisle 1",@"Aisle 2",@"Aisle 3", @"Deli",nil];
NSArray *unitNames = [NSArray arrayWithObjects:
@"g",@"pkt",@"box",@"ml",@"kg",nil];
NSArray *itemNames = [NSArray arrayWithObjects:
@"Grapes",@"Biscuits",@"Nappies",@"Shampoo",@"Sausages",nil];
int i = 0;
for (NSString *itemName in itemNames) {
LocationAtHome *locationAtHome =
[NSEntityDescription
insertNewObjectForEntityForName:@"LocationAtHome"
inManagedObjectContext:cdh.context];
LocationAtShop *locationAtShop =
[NSEntityDescription
insertNewObjectForEntityForName:@"LocationAtShop"
inManagedObjectContext:cdh.context];
Unit *unit =
[NSEntityDescription insertNewObjectForEntityForName:@"Unit"
inManagedObjectContext:cdh.context];
Item *item =
[NSEntityDescription insertNewObjectForEntityForName:@"Item"
inManagedObjectContext:cdh.context];

locationAtHome.storedIn = [homeLocations objectAtIndex:i];
locationAtShop.aisle = [shopLocations objectAtIndex:i];
unit.name = [unitNames objectAtIndex:i];
item.name = [itemNames objectAtIndex:i];

item.locationAtHome = locationAtHome;
item.locationAtShop = locationAtShop;
item.unit = unit;

i++;
}
[cdh saveContext];


4. Run the application once to insert the test data. The expected result is shown in Figure 5.9.

Image

Figure 5.9 PrepareTVC table view with test data

5. Remove all code within the demo method of AppDelegate.m to prevent further inserts of the same data.

Congratulations! The fundamental components of a Core Data–driven table view are now in place. The next steps for PrepareTVC involve adding the basic features a user would expect from this application, such as the ability to delete items or add them to the Shop tab when they’re selected.

Two additional table view data source methods are required:

image commitEditingStyle is responsible for handling item deletion, which happens when a user swipes a table view cell. Not only does it delete the item in question, it also ensures the table view row is removed, too.

image didSelectRowAtIndexPath is responsible for toggling whether or not an item is listed and thereby shown on the Shop tab. It also ensures items freshly set as listed are not set as collected. When an item is marked as collected, it will be “ticked off” yet still visible in the Shop tab. When a user taps Clear on the Shop tab, any item marked as collected will be removed from the Shop tab.

Listing 5.12 shows the two new methods required in the VIEW section.

Listing 5.12 PrepareTVC.m: VIEW (Selection and Deletion)


- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
if (editingStyle == UITableViewCellEditingStyleDelete) {
Item *deleteTarget = [self.frc objectAtIndexPath:indexPath];
[self.frc.managedObjectContext deleteObject:deleteTarget];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
}
}
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
NSManagedObjectID *itemid =
[[self.frc objectAtIndexPath:indexPath] objectID];

Item *item =
(Item*)[self.frc.managedObjectContext existingObjectWithID:itemid
error:nil];
if ([item.listed boolValue]) {
item.listed = [NSNumber numberWithBool:NO];
} else {
item.listed = [NSNumber numberWithBool:YES];
item.collected = [NSNumber numberWithBool:NO];
}
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}


Update Grocery Dude as follows to add to the existing VIEW section:

1. Add the code from Listing 5.12 to the bottom of the VIEW section of PrepareTVC.m.

2. Select Main.storyboard.

3. Select the Prototype Table View Cell of the Items Table View Controller and then open Attributes Inspector (Option+image+4), as shown in Figure 5.10.

Image

Figure 5.10 The Items table view prototype cell

4. Set the Table View Cell Selection to None, as shown in Figure 5.10.

5. Set the Table View Cell Accessory to Detail Disclosure, as shown in Figure 5.10.

Run the application again to see the new application behaviors. First, when an item is selected on the Prepare tab, it goes orange or gray. Once the ShopTVC table view is configured later in this chapter, orange items on the Prepare tab will be visible on the Shop tab. This is how an item is “added” to the shopping list.

The second new behavior is the ability to delete items by swiping them. Test this out by deleting the biscuits. Note that deletions won’t be saved to the persistent store until save is called on the context.

Interaction

The INTERACTION section of PrepareTVC is used to handle the new Clear button on the Prepare tab. This button will be used to remove all items displayed on the Shop tab. Because this button could be pressed accidentally, an action sheet will be used to confirm the action.

The ShopTVC table view will display the shopping list items, which means it will only show items where item.listed = YES. This calls for a new fetch request template to be created that will not only be used to populate the ShopTVC table view, it will also be used to unlist listed items when the Clear button is pressed.

Update Grocery Dude as follows to create the shopping list fetch request template:

1. Select Model 6.xcdatamodel.

2. Rename the existing Test fetch request template to ShoppingList and configure it as shown in Figure 5.11. This fetch request will only fetch items flagged as listed. As per usual for Boolean attributes, 1 is equal to YES.

Image

Figure 5.11 ShoppingList only fetches listed items.

The INTERACTION section will have three methods:

image clear is an interface builder action that will be linked to a new Clear button on the Items table view. Pressing this button will present a “Clear entire shopping list?” action sheet, provided there is at least one listed item.

image actionSheet is an action sheet delegate method used to handle the confirmation or cancellation of the clear action.

image clearList will iterate through all listed items, marking them as unlisted. This will have the effect of “removing” the items from the Shop tab.

Listing 5.13 shows these three new methods required in the INTERACTION section.

Listing 5.13 PrepareTVC.m: INTERACTION


#pragma mark - INTERACTION
- (IBAction)clear:(id)sender {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}

CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
NSFetchRequest *request =
[cdh.model fetchRequestTemplateForName:@"ShoppingList"];
NSArray *shoppingList =
[cdh.context executeFetchRequest:request error:nil];

if (shoppingList.count > 0) {

self.clearConfirmActionSheet =
[[UIActionSheet alloc] initWithTitle:@"Clear Entire Shopping List?"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:@"Clear"
otherButtonTitles:nil];
[self.clearConfirmActionSheet
showFromTabBar:self.navigationController.tabBarController.tabBar];
}
else {
UIAlertView *alert =
[[UIAlertView alloc] initWithTitle:@"Nothing to Clear"
message:@"Add items to the Shop tab by tapping them on the Prepare tab. Remove all items from the Shop tab by clicking Clear on the Prepare tab"
delegate:nil
cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
[alert show];
}
shoppingList = nil;
}
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex {

if (actionSheet == self.clearConfirmActionSheet) {
if (buttonIndex == [actionSheet destructiveButtonIndex]) {
[self performSelector:@selector(clearList)];
}
else if (buttonIndex == [actionSheet cancelButtonIndex]){
[actionSheet dismissWithClickedButtonIndex:
[actionSheet cancelButtonIndex] animated:YES];
}
}
}
- (void)clearList {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}

CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
NSFetchRequest *request =
[cdh.model fetchRequestTemplateForName:@"ShoppingList"];
NSArray *shoppingList =
[cdh.context executeFetchRequest:request error:nil];

for (Item *item in shoppingList) {
item.listed = [NSNumber numberWithBool:NO];
}
}


Update Grocery Dude as follows to implement the INTERACTION section:

1. Add the code from Listing 5.13 to the bottom of PrepareTVC.m before @end and then save the class file (press image+S).

2. Select Main.storyboard.

3. Drag a Bar Button Item to the top left of the Items table view.

4. Set the new Bar Item Title to Clear using Attributes Inspector (Option+image+4), as shown in Figure 5.12.

Image

Figure 5.12 The Clear button will remove items from the shopping list

5. Hold down Control and drag a line from the Clear button to the yellow circle at the bottom of the Items table view. Then select Sent Actions > clear: from the pop-up menu. This will link the Clear button to the clear method.

Run the application again to see the new application behaviors. If you press the Clear button and nothing is selected, a notification will tell you to select items before pressing Clear. If items are selected (orange) and Clear is pressed, you will have a chance to confirm or cancel that action. If you confirm the action, then all orange items will return to gray. Throughout all of this, the Shop tab will remain empty because it hasn’t yet been configured.

Introducing ShopTVC

The purpose of the ShopTVC table view is to show a shopping list sorted by where items are located in a shop. ShopTVC will be so similar to the PrepareTVC that some method implementations are exactly the same.

Update Grocery Dude as follows to create the ShopTVC class:

1. Ensure the Grocery Dude Table View Controllers group is selected.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to CoreDataTVC and Class name to ShopTVC. Click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Select Main.storyboard.

7. Set the Custom Class of the Table View Controller titled Grocery Dude to ShopTVC using the same approach shown previously in Figure 5.8.

The ShopTVC header will remain unchanged; however, the implementation file will need updating. The DATA, VIEW, and INTERACTION sections will be used again.

Data

The DATA section of the ShopTVC implementation contains only a configureFetch method. The code is the same as the equivalent PrepareTVC code, with the only difference being that a fetch request template is used to constrain the fetch results to listed items. Notice that the fetched results controller uses a copy of the fetch request template. This is because you can’t edit an existing fetch request template, which you need to do when specifying a sort descriptor. Everything else in the method shown in Listing 5.14 should be familiar.

Listing 5.14 ShopTVC.m: DATA


#import "ShopTVC.h"
#import "CoreDataHelper.h"
#import "Item.h"
#import "Unit.h"
#import "AppDelegate.h"

@implementation ShopTVC
#define debug 1

#pragma mark - DATA
- (void)configureFetch {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
NSFetchRequest *request =
[[cdh.model fetchRequestTemplateForName:@"ShoppingList"] copy];

request.sortDescriptors =
[NSArray arrayWithObjects:
[NSSortDescriptor sortDescriptorWithKey:@"locationAtShop.aisle"
ascending:YES],
[NSSortDescriptor sortDescriptorWithKey:@"name"
ascending:YES],
nil];
[request setFetchBatchSize:50];

self.frc =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:cdh.context
sectionNameKeyPath:@"locationAtShop.aisle"
cacheName:nil];
self.frc.delegate = self;
}
@end


Update Grocery Dude as follows to implement the DATA section in ShopTVC:

1. Replace all code in ShopTVC.m with the code from Listing 5.14.

View

The VIEW section is a familiar sight because the methods involved are again similar to the existing PrepareTVC methods. Here are the only differences:

image The viewDidLoad method doesn’t configure an action sheet delegate because it’s not required in ShopTVC.

image The cellForRowAtIndexPath method shows listed items in green with a tick if they’re marked as collected (otherwise in orange).

image The sectionIndexTitlesForTableView method hasn’t changed.

image The didSelectRowAtIndexPath method toggles whether or not an item is marked as collected. To a user, tapping a row ticks items off the shopping list.

image The commitEditingStyle method is not implemented in ShopTVC, which prevents a user from deleting items from the Shop tab. It may have been otherwise confusing to the user how to properly tick an item off the shopping list.

Listing 5.15 shows the VIEW section code.

Listing 5.15 ShopTVC.m: VIEW


#pragma mark - VIEW
- (void)viewDidLoad {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
[super viewDidLoad];
[self configureFetch];
[self performFetch];

// Respond to changes in underlying store
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(performFetch)
name:@"SomethingChanged"
object:nil];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
static NSString *cellIdentifier = @"Shop Cell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];

Item *item = [self.frc objectAtIndexPath:indexPath];
NSMutableString *title = [NSMutableString stringWithFormat:@"%@%@ %@",
item.quantity, item.unit.name, item.name];
[title replaceOccurrencesOfString:@"(null)"
withString:@""
options:0
range:NSMakeRange(0, [title length])];
cell.textLabel.text = title;

// make collected items green
if (item.collected.boolValue) {
[cell.textLabel setFont:[UIFont
fontWithName:@"Helvetica Neue" size:16]];
[cell.textLabel setTextColor:
[UIColor colorWithRed:0.368627450
green:0.741176470
blue:0.349019607 alpha:1.0]];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
else {
[cell.textLabel setFont:[UIFont
fontWithName:@"Helvetica Neue" size:18]];
cell.textLabel.textColor = [UIColor orangeColor];
cell.accessoryType = UITableViewCellAccessoryDetailButton;
}
return cell;
}
- (NSArray*)sectionIndexTitlesForTableView:(UITableView *)tableView {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
return nil; // prevent section index.
}
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
Item *item = [self.frc objectAtIndexPath:indexPath];
if (item.collected.boolValue) {
item.collected = [NSNumber numberWithBool:NO];
}
else {
item.collected = [NSNumber numberWithBool:YES];
}
[self.tableView reloadRowsAtIndexPaths:
[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}


Update Grocery Dude as follows to add the VIEW section:

1. Add the code from Listing 5.15 to the bottom of ShopTVC.m before @end.

Interaction

The INTERACTION section of ShopTVC is used to handle another Clear button. The difference between the Clear buttons on each tab is that the one on the Prepare tab clears the whole shopping list, whereas the one on the Shop tab clears only collected items. Again, a clear method will be executed when the Clear button is tapped. As shown in Listing 5.16, this method either clears collected items or alerts the user that there’s nothing to clear.

Listing 5.16 ShopTVC.m: INTERACTION


#pragma mark - INTERACTION
- (IBAction)clear:(id)sender {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
if ([self.frc.fetchedObjects count] == 0) {

UIAlertView *alert =
[[UIAlertView alloc] initWithTitle:@"Nothing to Clear"
message:@"Add items using the Prepare tab"
delegate:nil
cancelButtonTitle:@"Ok" otherButtonTitles:nil];
[alert show];
return;
}
BOOL nothingCleared = YES;
for (Item *item in self.frc.fetchedObjects) {

if (item.collected.boolValue)
{
item.listed = [NSNumber numberWithBool:NO];
item.collected = [NSNumber numberWithBool:NO];
nothingCleared = NO;
}
}
if (nothingCleared) {
UIAlertView *alert =
[[UIAlertView alloc] initWithTitle:nil message:
@"Select items to be removed from the list before pressing Clear"
delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil];
[alert show];
}
}


Update Grocery Dude as follows to implement the INTERACTION section:

1. Add the code from Listing 5.16 to the bottom of ShopTVC.m before @end and then save the class file (press image+S).

2. Select Main.storyboard.

3. Drag a Bar Button Item to the top left of the Grocery Dude table view and then set its Bar Item Title to Clear using the same approach used with the Clear button on the Items view.

4. Hold down Control and drag a line from the new Clear button to the yellow circle at the bottom of the Grocery Dude table view. Then select Sent Actions > clear: from the pop-up menu. This will link the Clear button to the clear method.

5. Select the Prototype Table View Cell on the Grocery Dude Table View Controller.

6. Set the Table View Cell Style to Basic.

7. Set the Table View Cell Selection to None.

8. Set the Table View Cell Accessory to Detail Disclosure.

Run the application and ensure some items on the Prepare tab are orange. Change to the Shop tab and notice these items have appeared sorted by aisle! Tap items on the Shop tab and they will go green and ticked. Press the Clear button on the Shop tab to remove the ticked items from the Shop tab. Return to the Prepare tab and notice the bought items (collected and cleared) aren’t orange anymore.

Summary

The application has really started to take shape as core functionality has been implemented. Already you can see the Prepare and Shop tabs listing potential items and shopping list items, respectively. To get to this point, certain repeatable design patterns were followed as you’ve seen with the implementation of the PrepareTVC and ShopTVC classes. By modifying the configureFetch method, you’ll be able to leverage these design patterns in your own projects. Note that debug is enabled in all of the TVC classes, so performance of the application will be slower than usual for the time being.

Exercises

Why not build on what you’ve learned by experimenting?

1. Try changing the sectionNameKeyPath and the sort descriptor in the configureFetch method of PrepareTVC.m to name. If implemented correctly, this will group the data into sections by name.

2. Delete the sectionIndexTitlesForTableView method from PrepareTVC.m and then run the application. You should see a section index in the Items table view.

3. Reinstate the code from Listing 5.11 into the demo of AppDelegate. Run the application three times to insert triplicate data. Examine the console log as you scroll up and down in the Items table view. As you scroll, you should notice the methods cellForRowAtIndexPath andtitleForHeaderInSection repeatedly appearing in the console log. This is the table view efficiently reusing cells to give the illusion that all data is in memory and waiting to be displayed.

Reverse any changes you’ve made to the project during the exercises before continuing to the next chapter.