Saturday, 26 April 2014

Table Cell Caching [Table Manners]

In the rubber stamp analogy, I said that the table view “gives you a rubber stamp and asks you to configure it.” I lied—at least a little. The table view doesnt give you the cell object to use because it doesnt know what kind ofcell object you need. Instead, a cell object is created by either the storyboard or code you write, and the table view hangs onto it so you can reuse it again next time. This is called the table cell cache.

There are three ways of using the table cell cache:

n     Let your storyboard create the cell objects

n     Lazily create cell objects programmatically, as needed

n     Ignore the cache entirely

In this app, you’ll take the first approach. The Master Detail project template has already defined a single table cell object, with the unimaginative identifier "Cell". Select the Main_iPhone.storyboard file and select the table view object in the Master  View Controller scene, as shown in Figure 5-16.


Figure 5-16. Table view with prototype table cell

At the top of the table view you’ll see a Prototype Cells region. This is where Interface Builder lets you design the cell objects that your table view will use. The Prototype Cells count (shown in the attributes inspector on the rightof Figure 5-16) declares how many different cell objects your table needs. You only need one.

Click on the one and only prototype cell template, as shown in Figure 5-17. Now you’re editing a single table cell object. Notice that the Identifier property is set to Cell this identifies
the cell in the cache and must exactly match the identifier you pass in the
-dequeueReusableCellWithIdentifier:forIndexPath: message.


Figure 5-17. Editing a table cell prototype


Your table will display the name of the object and its location. The standard cell type that fits that description is the subtitle style (UITableViewCellStyleSubtitle). Change the cells style to Subtitle, as shown in the upper-right ofFigure 5-17.
Your table view design is complete. You’ve defined a single cell object, with an identifier of “Cell”, that uses the subtitle table cell style.

The table view cell cache makes it easy for your -tableView:cellForRowAtIndexPath: method to efficiently reuse table cell view objects, and there are a variety of different ways to use it.

At one extreme, you don’t have to use the cache at all. Your -tableView:cellForRowAtIndexPath: method could return a new cell object every time its called. This would be appropriate for a tiny number of rows, where each rowwas completely different—the  kind of interface you see in the Settings app, for example.

An alternative, and the more traditional, way of using the table cell cache is to programmatically create your table cell view objects, as needed. This is also called lazy  object creation. You do this by checking to see if thecell object you need is already in the cache and create one only if it isn’t. The code to do that looks like this:

id cellIdent = @"LazyCell";   // choose  appropriate cell  here UITableViewCell   *cell;
cell = [tableView dequeueReusableCellWithIdentifier:cellIdent]; if (cell==nil)
{
cell = [[UITableViewCell alloc]  initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdent];
// one-time cell  view  configuration goes  here
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}

This code asks the table cell cache if a cell with that identifier has already been added. If not, the message will return nil, indicating theres no such object in the cache. Your code responds by creating a new cell object,assigning it the same cell identifier. When you return this cell object to the table view, it will automatically add it to its cache. Next time, that cell view object will be in the cache.

A third alternative is to register a cell view class or Interface Builder file with the table using the -registerClass:for CellReuseIdentifier: or -registerNib:forCellReuseIdentifier: messages. After you do that, requests for a cell with thatidentifier thats not in the cache will automatically create one for you. (This is, essentially, what happens when you design prototype cells using the storyboard.)

Using cell identifiers, you can also maintain a small stable of different cell objects. In your MyStuff app, you might one day decide to have a different row design for Star Wars memorabilia and another row design for stuffyou got from your grandmother. You would assign each cell object its own identifier  (@"Cell", @"Star  Wars" @"Me Ma"). The table view cell cache would then keep all three cell objects, returning the appropriate one whenyou send it the-dequeueReusableCellWithIdentifier: message. To do this using a storyboard, set the Prototype Cells count to 3 and assign a unique identifier to each prototype cell.

You are free to mix and match any of these techniques. A single table could have some cell view objects that are defined in the storyboard, others registered to be created by class name, and your code could lazily create therest.

While you’re here, change the name in the navigation bar from Master  to My  Stuff. Do this by
double-clicking on the “Master” title in navigation bar above the table view (see Figure 5-17). You’ve now implemented all of the code needed to display your MyWhatsit objects in a table view. Theres only one thing missing...


Wheres the Data?

You can run your app right now, but it wont display anything. Thats because you dont have any MyWhatsit objects to display. To make things worse, you havent written any of the code to create or edit objects yet, so if youtried to create one your app will just crash.

My solution in these situations is to cheat; programmatically create a few test objects so the interface has something to display. Find the -awakeFromNib method in MSMasterViewController.m. This message is sent to any objectcreated by an Interface Builder file. It gives you an opportunity to do any additional setup that couldnt be accomplished in Interface Builder.

The last statement in that method will be [super awakeFromNib]. Immediately before that, add this code (in bold):

things = [@[
[[MyWhatsit  alloc] initWithName:@"Gort" location:@"den"],
[[MyWhatsit  alloc] initWithName:@"Disappearing TARDIS  mug" location:@"kitchen"],
[[MyWhatsit  alloc] initWithName:@"Robot   USB drive" location:@"office"],
[[MyWhatsit  alloc] initWithName:@"Sad  Robot USB  hub" location:@"office"],
[[MyWhatsit  alloc] initWithName:@"Solar Powered Bunny" location:@"office"]
] mutableCopy];
[super  awakeFromNib];

This code creates five new MyWhatsit objects and assembles them into an array object. A mutable version of that array is then assigned to the things property variable. Now when your controller is first created, it will have a setof MyWhatsit objects to show.


Testing MyStuff

Set your scheme to one of the iPhone Simulators and run your app. Your table view of MyWhatsit
objects appear, as shown on the left in Figure 5-18.


Figure 5-18. Working table view

Thats pretty cool! You’ve created your own data model object and implemented everything required to display your custom set of objects in a table view, using a cell format of your choosing.

But its clear that this app isnt finished yet. If you tap one of the rows, you get a new screen (on the right in Figure 5-18) that doesnt make a lot of sense, and certainly isnt part of your design.

The next step is to design your details view. After that, you’ll implement the code needed to edit the list and individual items.


Adding the Detail View

Now you’re at the second half of the Master Detail design. Your detail view is controlled by the
MSDetailViewController object. MSDetailViewController is a plain old UIViewController that
load the view objects in the Detail View Controller scene. You need to create label and text field objects to display and edit your MyWhatsit properties. You’ll need to create Interface Builder outlets
in MSDetailViewController to connect with those text fields, and you’ll need to connect them to their objects in Interface Builder. This should be familiar territory by now, so lets get started.


Creating the Detail View

Start with the iPhone interface. Select the Main_iPhone.storyboard file and then select the Detail View Controller object, as shown in Figure 5-19. Select and delete the label object in the viewYou dont need it.


Figure 5-19. Template detail view


In the object library, locate the label object and add two of them to your view. Find the text field object and add two of those. Set the text of one label to Name and the other to Location. Arrange and resize them so your interface looks like the one in Figure 5-20. Choose the Editor  Resolve Auto Layout Issues  Reset to Suggested Constraints in Detail View Controller command.


Figure 5-20. Finished detail view

Switch to the MSDetailViewController.h interface file. Underneath the #import statement, add another (so the compiler knows about your MyWhatsit class):

#import  "MyWhatsit.h"

Change the type of the detailItem property so its specifically a MyWhatsit object:

@property  (strong,nonatomic) MyWhatsit *detailItem;

Delete the existing detailDescriptionLabel property and replace it with two new outlet properties:

@property  (weak,nonatomic) IBOutlet  UITextField *nameField;
@property  (weak,nonatomic) IBOutlet  UITextField *locationField;

Switch back to the Main_iPhone.storyboard file. Select the Detail View Controller object and use the connections inspector to connect the two new outlets (nameField and locationField) to the appropriate text field objects in theinterface, as shown in
Figure 5-21.


Figure 5-21. Connecting the text field outlets

Configuring the Detail View

You might be asking how the values of a MyWhatsit object are going to get into the two UITextField
objects you just created. Thats an excellent question. Its going to happen when the user taps on row in the table view. Most of the code to get from that tap to your detail view has already been written for you (as part of theMaster Detail Xcode template), but its important to understand how it all works. Lets walk through the process of tapping on a row.

On the iPhone, tapping a row triggers a “push” seque that slides the detail view onto the screen. This push seque—the arrow connecting the master view to the detail view, shown in Figure 5-19—was
pre-defined as part of the Master Detail project template. You can create your own seques by control/right-dragging  from a prototype cell to the scene you want that cell to navigate to.

Just before a seque occurs, your view controller receives a -prepareForSeque:sender: message. Find that method in your MSMasterViewController.m file now. All seques from this view to another view send the same message. Byexamining the seque.identifier property, you can determine which seque is occurring—assuming you assigned each seque a unique identifier. In this case, you’re interested in the "showDetail" seque.

The next step is to prepare the new view to be displayed. The existing code gets the object to edit from the things array. Unfortunately, this is template code that thinks there are NSDate objects in the array. Change it so its aMyWhatsit object, like this (modified code in bold):

MyWhatsit *object = things[indexPath.row];

The rest of the code doesnt require any modification. It takes the MyWhatsit object and uses it to set the detailItem of the destination view controller, which you know to be the MSDetailViewController. Since you already changedthe type of the detailItem to MyWhatsit, the two object types agree and the compiler warning disappears.

In the case of the iPad, the code path is a little different. There is no seque on the iPad because both the master list and the detail view are visible simultaneously. Click on the Main_iPad.storyboard
file and notice that the cell prototype has no seque or disclosure accessory. Instead, your master
view controller will intercept the user tapping on a cell in the list and update the detail view (which is already visible).

Whenever your user taps on a cell, the table views delegate object receives a -tableView:did SelectRowAtIndexPath: message. Find this method in your MSMasterViewController.m file. The existing code first determines if thisis an iPad or not. If it is, it performs the exact same task
that -prepareForSeque:sender: does. Its also just as wrong. Make the same edit that you did in
-prepareForSeque:sender:, changing the NSDate to MyWhatsit.

Following the chain of events, find the -setDetailItem: method in the MSDetailViewController.m
implementation file. This is the message the MSDetailViewController receives when you assign
a value to the detailItem property (that is, when self.detailViewController.detailItem = object executes).

The -setDetailItem: method sets its internal _detailItem variable with the new object to display. It then sends itself a -configureView message. This is the message you’re looking for (even if you didnt know it yet). The rest of the -setDetailItem: message handles the case where the item was chosen from a pop-over list on the iPad interface.

The -configureView message is received whenever your detail view controller needs to prepare itself to display a new MyWhatsit object. This is the method you need to rewrite so your MyWhatsit property values will appear in your interface. Edit -configureView so it looks like this:

-  (void)configureView
{
if (self.detailItem!=nil)
{
self.nameField.text = self.detailItem.name; self.locationField.text = self.detailItem.location;
}
}

This new method checks to see if a detailItem object has been set. If it has, it uses the nameField and locationField connections to set the contents of its two text fields to the values of the name and location properties of theMyWhatsit object.

This completes the (iPhone) detail view! When the user taps a row, the delegate gets the MyWhatsit object for that row, passes it to your MSDetailViewController, which sends itself -configureView to make those values appear in the view. The MSDetailViewController then becomes the active view and viola, your detail appears, as shown on the right in Figure 5-22.


Figure 5-22. Working detail view

The only thing left to do is flesh out the iPad detail view. Like in previous projects, most of the work has already been done. In your Main_iPhone.storyboard file, copy the four view objects you just added; drag out a rectangle toselect the two label and two text fields, and choose
Edit  Copy. Now switch to your Main_iPad.storyboard file. Delete the existing label object, click
once in the blank view so Xcode knows where to paste your objects, and choose Edit  Paste.
Reposition and resize them so they fit the iPad interface, as shown in Figure 5-23. Choose Reset to Suggested Constraints in Detail View Controller from the Resolve Auto Layout Issues button. Select the Detail ViewController and use the connections inspector to connect the nameField  and locationField 
outlets.


Figure 5-23. Finished iPad detail view

You’ll also need to make the same changes in the iPads table view that you made in the iPhones table view. Find the Master  View Controller scene (the one with the table view). Select the prototype table cell view object andchange its style to Subtitle (see Figure 5-17). Change the navigation title from “Master” to “My Stuff”.

Now run your project in an iPad simulator. The iPad interface is considerably different. In portrait mode, you see the detail view instead of the table view (on the left in Figure 5-24). You get to the table view via the Masterbutton (in the middle of Figure 5-24).


Figure 5-24. MyStuff running on an iPad

If you turn the iPad on its side (choose Hardware  Rotate Left in the simulator), you get a split view with your table view on the left and your detail view on the right (on the right of Figure 5-24).

You may notice that you can edit the text fields, but they dont change anything. The last part of your app development will be to set up editing of your MyWhatsit objects, allow the user to create new ones, change them, anddelete ones they dont want.

No comments:

Post a Comment