Showing posts with label UIViewController. Show all posts
Showing posts with label UIViewController. Show all posts

Wednesday, May 7, 2014

Bringing It All Together: Notifications

There are some routine activities in iOS development that involve spreading related code across multiple methods. In my early days, I just took this for granted as The Way Things Are Done. As my skills and experience grew, some of these aspects started to rub me the wrong way, and eventually I became irritated enough to come up with my own solutions. So over the next couple posts, I'd like to describe those solutions and explain how they came to be.

 

The first topic is notifications, specifically setting up a UIViewController subclass to listen for them. I was initially set along the path of "something needs to change" by this post. Here the author argues against the use of what I call "Nuclear Unregister" in viewDidDisappear:, like so:

 

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

   [[NSNotificationCenter defaultCenter] removeObserver:self];
}

 

The author states - and I wholeheartedly agree - that this should only be done in dealloc. He describes how doing this in a UIViewController subclass also winds up unsubscribing from any notifications that UIViewController would be otherwise listening for, such as memory warnings. I think Apple has made some internal changes since this was written 3 years ago, as I was indeed still able to get memory warning notifications after doing this in iOS 7, but the larger point remains. Maybe the superclass is depending on some other notifications that are still in play when the view is off screen. Maybe future subclasses will, too. Going with the nuclear option means you are turning off listeners that you are not responsible for and possibly have no control over, and this happens at a point when the object in question is very much still alive and will likely be used again in the future. At minimum it is lazy, but on the other end of the spectrum you are stepping on toes that shouldn't be stepped on. Sure, your listeners will be reactivated in viewWillAppear, but if sub/superclasses had them defined in -init, they won't be coming back on again.

 

Whether or not you agree that this is an issue - and by this point, I certainly hope that you do - the argument was convincing enough for me to revisit lots of code where I had been doing it wrong. And I then moved forward with a strict policy of only unregistering the specific notifications that I had originally registered.

 

This means that NSNotificationCenter code gets spread out to numerous locations. The vast majority of them will be added in viewWill/DidAppear:

 

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

   NSNotificationCenter *notificationCenter = ...;
   [notificationCenter addObserver: selector: name: object: ];
   [notificationCenter addObserver: selector: name: object: ];
   [notificationCenter addObserver: selector: name: object: ];
   [notificationCenter addObserver: selector: name: object: ];
}

 

And then you have to remember to turn them off in viewWill/DidDisappear:

 

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

   NSNotificationCenter *notificationCenter = ...;
   [notificationCenter removeObserver: name: object: ];
   [notificationCenter removeObserver: name: object: ];
   [notificationCenter removeObserver: name: object: ];
   [notificationCenter removeObserver: name: object: ];
}

 

And this is just for the ones that need to be active only when on screen. If you need more that will be active for the life of this object, then you have to handle init. But if this view controller is created in a XIB file, it won't run through init, so you have to cover -awakeFromNib too. And then of course you do the nuclear unregister in dealloc. So that is potentially 5 different methods where some code will be placed. This is spreading things out too much for me.

 

Worse, there is nothing inherently linking any of these locations. Say you change your mind, and one of these viewDidAppear ones needs to go to init instead. Now you have to remember to remove the corresponding line from viewDidDisappear, because if you don't it could potentially be shut off early. Are you going to remember to do that every time? Will the new person on the project even know that they should? It is related code in multiple methods that contains no additional documentation about what the intent should be.

 

Also it's annoying to have to type out the removeObserver: lines again. Even if you copy-paste from the addObserver: lines, you still have to edit each one. The 3 parameters in removeObserver: are exactly the same as 3-out-of-4 of the addObserver: parameters, but you don't get any code-completion help typing out the combination. It's a situation just begging for mistakes.

 

That pretty much covers all of my pain points with notifications, so let's move on to my attempt to address them. I was not able to come up with a single class or technique that solves all of these issues. They are similar-but-different method calls that have to happen. They will need to be called at different times. It's a tough nut to crack, but I came up with a system that I feel does a really good job of making this process as simple as it can be. The classes involved are part of my BTIKit project, so you can just hop over there and look at code if you don't care to read any more.

 

Step 1 is basically to refactor the addObserver/removeObserver methods. If it can be broken down to as simple as "startListening" and "stopListening" that would be great. Naturally these methods do not contain enough information, as we're dealing with 3 or 4 parameters ultimately, so I need an object that can store the appropriate values until they are needed. Then they can be used by these simpler methods.

 

Enter BTINotificationInfo.

 

This class has properties for storing the observer, the selector, the name, and the object. But those are all private as they shouldn't be messed with after this object is created, so let's talk just about the initializer:

 

+ (instancetype)infoWithObserver:(id)observer
   selector:(SEL)selector
   name:(NSString *)name
   object:(id)object
   lifespan:(BTINotificationInfoLifespan)lifespan;

 

One of these objects is created with the exact same parameters that would be involved with addObserver. There is an additional lifespan value that I'll discuss in a moment. Then we do indeed have our simpler methods:

 

- (void)startListening;
- (void)stopListening;

 

The initializer places the parameters into the appropriate properties, and then these methods reference them as needed to do the addObserver/removeObserver stuff. Right off the bat, one potential source of error has been removed: the largely-duplicated parameters in removeObserver. Instead of appearing twice (add + remove), they appear once. The values are defined in one place. Then using this object, you can start/stop listening as much as you want, repeatedly, without having to type those values again.

 

Of course, add/remove observer are called from different methods typically, so you will want to keep this object around somewhere. As I said, I was not able to come up with a single-class solution. But this does serve as the building block for the entire system. And this object can be used standalone. An example would be in Core Data, where you might start listening for a managed object context save, do the save, and then stop listening. Rather than type out the parameters twice, just create one of these objects. startListening before, save, stopListening when finished. Easy.

 

I mentioned the additional lifespan parameter. With view controllers, the vast majority of notification needs fall into 2 categories: 1) Listen only when the view is on screen, or 2) Listen the entire time this view controller is alive. If there have been other situations, they didn't sink into my mind enough to care about. And then I haven't really had too many consistent notification needs outside of view controllers that follow any particular pattern. So the rest of the solution is geared towards view controllers, and this lifespan parameter is how you define which category you fall into. The value is an enum, with pre-defined values of "lifetime", "visible", and "custom". While defining all of the notification listener stuff, you can also define the period during which this listener should be active.

 

This addresses another one of my pain points. Instead of relying on the addObserver code to be in a particular method to explain the intent, now you explicitly define it. And it is easy to change your mind. The case I mentioned previously about moving the addObserver from viewDidAppear to init goes away. Change the lifespan value, done. Nothing else needs to change.

The BTINotificationInfo object doesn't use the lifespan value itself. Actually this is the only property that is allowed to be public (read-only), so that other classes can make use of it. For the vast majority of my needs, "other classes" have been view controllers. And a view controller makes up the other half of this solution: BTIViewController.

 

The first thing that BTIViewController provides is a single place to define ALL (or at least "most", certainly) of your BTINotificationInfo objects:

 

- (void)populateNotificationInfos NS_REQUIRES_SUPER;

 

This is subclass-friendly, so any view controllers subclassing this can define their own notification infos. This method will be automatically called when the view controller is created.

 

BTINotificationInfo has that lifespan property, so you can define all of your "visible" and "lifetime" notifications right along side each other, in the same method. Since everything is defined in one place, that reduces the amount of effort required to investigate all of your notification listener setup.

 

Two simple methods make the magic happen:

 

- (void)startListeningForNotificationsWithLifespan:(BTINotificationInfoLifespan)lifespan;
- (void)stopListeningForNotificationsWithLifespan:(BTINotificationInfoLifespan)lifespan;

 

These can be called manually if desired, but there probably isn't much of a reason to do so. BTIViewController is already set up to register the "lifetime" ones in init/awakeFromNib, and handle the "visible" ones in viewWill(Dis)Appear. Pretty much the only reason to call these methods manually would be to address any "custom" ones, but I will make a guess that it won't make much sense to handle special-case ones along with the others. I could be wrong.

 

BTIViewController has a primary method for adding notification infos:

 

- (void)addNotificationInfo:(BTINotificationInfo *)info;

 

...and then a whole bunch of shorthand helper methods that reduce the amount of code required even further. Let's look at a typical example:

 

[[NSNotificationCenter defaultCenter] addObserver:self
   selector:@selector(keyboardWillShow:)
   name:UIKeyboardWillShowNotification
   object:nil];

 

To do this same thing with the generic methods would be:

 

BTINotificationInfo *notificationInfo = [BTINotificationInfo infoWithObserver:self
   selector:@selector(keyboardWillShow:)
   name:UIKeyboardWillShowNotification
   object:nil
   lifespan:BTINotificationInfoLifespanVisible];
[self addNotificationInfo:notificationInfo];

 

Ok, so it's a bit wordier. Here come the shorthand methods to save the day:

 

[self addVisibleNotificationInfoForName:UIKeyboardWillShowNotification
   selector:@selector(keyboardWillShow:)
   object:nil];

 

Boom, less code. The shorthand methods assume that the view controller - self - will be the observer. And the lifespan is incorporated in the name of the method.

 

One thing I haven't talked about yet is block-based notification handlers. For one thing, I never considered separate methods for notification responders to be a big deal, so they don't really solve a significant problem for me. For another, you have to keep track of the goofy observer object in order to removeObserver. So between solving a problem I didn't think I had and actually creating some work I didn't otherwise need to do, I didn't give them much thought.

 

Then I realized that with BTINotificationInfo I'm already keeping around an object for the purpose of unregistering. Since I'm doing that, that object can keep track of the observer object, too. And suddenly block-based notification handlers got more interesting, so I decided to support them and also included shorthand methods in BTIViewController. Like so:

 

[self addVisibleNotificationInfoForName:UIKeyboardWillShowNotification
   object:nil
   usingBlock:^(NSNotification *notification) {
      // Resize/reposition some views
   }];

 

We've pretty much got the holy grail here. Everything, and I mean everything, defined in one place.

 

So the steps that you need to take in order to experience this awesomeness are:

  • Acquire BTIKit.
  • Make your view controllers inherit from BTIViewController.
  • Override -populateNotificationInfos. Call super first.
  • Define the notifications that you care about.

 

That's it. BTIViewController will do the rest for you. No more typing essentially duplicated notification code in multiple methods. No more scrolling back and forth in your implementation file trying to find which notifications this class cares about. BTIViewController even does the nuclear unregister in dealloc, so you don't have to include it yourself anymore.

 

Enjoy.

Tuesday, July 10, 2012

UITableView How-To: Part 2 - Search

Part 1 | Part 3 | Part 4 | Part 5Part 6

 

If you have a lengthy list of data, you should provide the ability for your users to search through that data. Starting with OS 3.0, Apple made integrating search into table views easy enough that there really isn't a good reason not to include it.

 

This post is based heavily, if not entirely, on Apple's sample project called TableSearch.

 

UISearchDisplayController

 

The search UI that Apple provides is basically a table view with a search bar. They provide some pretty animations, such as sliding the search bar up to cover up the navigation bar (handy for maximizing available space, especially in landscape). The search table view is overlaid onto your existing view, so both your original table and the search table need data. The principles of delegate and data source as discussed in the last post still apply.

 

In addition to creating the controller itself, Apple also gave UIViewController a searchDisplayController property, which makes it easier to access. This obviously isn't populated by default, but be aware that it exists.

 

 

Data

 

Say you have a list of 10 items. The user performs a search, and only 2 items meet the criteria. Now think back to our datasource and delegate methods. We are answering a lot of questions about the table: how many sections, how many rows, what cell should be displayed (and what should the cell contain)? We still have to answer those questions, only now there are two tables involved. Even if there was only a single table, something would still have to be done with the data. There are two basic approaches you can take: 1) Remove items that don't match, or 2) Create a whole new list containing only items that do match. If you go with #1, you need some way of restoring the full list. So really, in either case, you are talking about 2 sources of data: the full list, and the search results. Since a separate table is involved for the search results, #2 probably makes more sense, and is what Apple shows in their demo.

 

Step Up To The Bar

 

We'll start with the easiest part of this project. Open up the view controller XIB in Interface Builder, and make sure you can see the table view. Find the Search Bar And Search Display Controller item in the library.

 

 

Do note that this is a separate choice from the standalone search bar. You can certainly roll your own solution using just the bar, but the controller is what makes the work relatively easy. So be sure to grab the one with the little orange circle.

 

In order to get the search bar to scroll with the table, we are going to add it as the table's header. Grab the library item, and drag to the upper portion of the table view. You should see a blue highlight, which is your confirmation that you will get the header.

 

 

A number of things happen automatically when you do this. Let's take a quick look at the object list:

 

 

Even though we didn't directly place it there, the Search Display Controller has been added. If we inspect the connections...

 

 

…we see a whole host of additions. The searchDisplayController property on the UIViewController has been populated. The necessary delegate and datasource connections have been made for the search controller, and the delegate has also been specified for the search bar itself. Not bad for one drag-n-drop operation. We are now done with IB, the rest is handled in code.

 

Changes to .h

 

Here is the end result, then I'll explain what's going on.

 

//  SampleViewController.h

#import <UIKit/UIKit.h>

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UISearchDisplayDelegate, UISearchBarDelegate>
{
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) NSMutableArray *contentsList;
@property (nonatomic, retain) NSMutableArray *searchResults;
@property (nonatomic, copy) NSString *savedSearchTerm;

- (void)handleSearchForTerm:(NSString *)searchTerm;

@end

 

First, we conform to the UISearchDisplayDelegate and UISearchBarDelegate protocols. Same idea as what we did for the table view previously.

 

Next we declare a couple of new instance variables and properties. The searchResults array will hold items that match the search criteria. The savedSearchTerm is something that Apple shows in their sample, and they use it to restore the search when returning to this screen.

 

Finally, we declare a method that will do the grunt work of searching through the data.

 

Changes to .m

 

We'll start at the top and work our way down. First, synthesize properties and handle memory management duties.

 

@implementation SampleViewController

@synthesize mainTableView = ivMainTableView;
@synthesize contentsList = ivContentsList;
@synthesize searchResults = ivSearchResults;
@synthesize savedSearchTerm = ivSavedSearchTerm;

- (void)dealloc
{
[self setMainTableView:nil];
[self setContentsList:nil];
[self setSearchResults:nil];
[self setSavedSearchTerm:nil];

[super dealloc];
}

- (void)viewDidUnload
{
[super viewDidUnload];

// Save the state of the search UI so that it can be restored if the view is re-created.
[self setSavedSearchTerm:[[[self searchDisplayController] searchBar] text]];

[self setSearchResults:nil];
}

 

That last bit is from Apple's sample. The counterpart is here:

 

- (void)viewDidLoad
{
[super viewDidLoad];

...

// Restore search term
if ([self savedSearchTerm])
{
[[[self searchDisplayController] searchBar] setText:[self savedSearchTerm]];
}
}

 

Saving/restoring the search criteria, nothing fancy. Then we get to the search routine itself:

 

- (void)handleSearchForTerm:(NSString *)searchTerm
{
[self setSavedSearchTerm:searchTerm];

if ([self searchResults] == nil)
{
NSMutableArray *array = [[NSMutableArray alloc] init];
[self setSearchResults:array];
[array release], array = nil;
}

[[self searchResults] removeAllObjects];

if ([[self savedSearchTerm] length] != 0)
{
for (NSString *currentString in [self contentsList])
{
if ([currentString rangeOfString:searchTerm options:NSCaseInsensitiveSearch].location != NSNotFound)
{
[[self searchResults] addObject:currentString];
}
}
}
}

 

First we store the search term. Then we lazily create the searchResults array if needed. Next we clear out any previous search results. Then we loop through our main data, find any matching items, and add them to the searchResults array.

 

This method could vary greatly depending on what kind of data you are working with, and how it is arranged. This particular implementation is based on an example in the Mark/LaMarche book.

 

Now we get to the fun part: the table datasource and delegate methods. We've discussed a bit already about the need to have 2 separate data lists, and pointed out that we will be dealing with 2 separate tables. However, we only have one set of delegate methods here in the controller. We could use a completely separate object, as mentioned in the previous post, but that's not really necessary. So, we need some way of determining which set of data to use. You might get away with using some kind of flag, say BOOL isCurrentlySearching or something along those lines. I have fought enough battles with the search display controller in attempting to do other tasks to know that this approach won't work. Thus, we'll go with the way Apple's sample shows, and that is to make decisions based on which table is asking for information. In hindsight, this is a really obvious approach, but I'm not always on the ball as quickly as I should be.

 

First, the number of rows:

 

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

if (tableView == [[self searchDisplayController] searchResultsTableView])
rows = [[self searchResults] count];
else
rows = [[self contentsList] count];

return rows;
}

 

The important thing to realize here is that all of these table view delegate methods include the calling table view itself as a parameter. This allows you the means to identify which table is making the request. If you had a reason to design a view with multiple tables, this is exactly what you would do. If the table asking for info is the search table, we provide an answer based on the search list, otherwise we use our main list.

 

We do the exact same thing when providing a cell:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger row = [indexPath row];
NSString *contentForThisRow = nil;

if (tableView == [[self searchDisplayController] searchResultsTableView])
contentForThisRow = [[self searchResults] objectAtIndex:row];
else
contentForThisRow = [[self contentsList] objectAtIndex:row];

static NSString *CellIdentifier = @"CellIdentifier";

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

[[cell textLabel] setText:contentForThisRow];

return cell;
}

 

Constructing the cell itself doesn't need to change just because we're using a search table. The key thing is to make sure we're grabbing the right piece of information to populate that cell. "Green" might be at row 4 in our main list, but it could be at row 2 in the search results. Once again, we make a decision based on which table is asking, and grab data from the appropriate list.

 

I'm not really going into cell selection yet, but you would need to do something similar in tableView:didSelectRowAtIndexPath:. The index path can, and most likely will, be different depending on which table is being shown.

 

At last, we reach the part that makes this all work. We've been dealing with table delegate methods up to this point, but the search controller has delegate methods of its own. So we'll utilize a couple of those to make the actual search happen. First, to begin the search:

 

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchString:(NSString *)searchString
{
[self handleSearchForTerm:searchString];

return YES;
}

 

And that's it. The documentation notes this is an optional method, and NOT implementing it will cause the search table to reload as the search term changes. So the only reason we're doing this is to define what logic should be performed in response to the search string. And this next part is also optional, but if you want to do any cleanup after the search, this is where you could do it:

 

- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
[self setSavedSearchTerm:nil];

[[self mainTableView] reloadData];
}

 

We no longer need the search term to be saved, so get rid of it. And maybe it is appropriate to refresh the main table view.

 

That's pretty much all there is to it at a basic level. Obviously this is a simple app so far, but the search display controller handles a lot duties on its own. Here is the sample project for everything so far:

 

TableViewTutorial_Part2.zip

UITableView How-To: Part 1 - View Controller Setup

Part 2 | Part 3 | Part 4 | Part 5 | Part 6

 

A while back, I was preparing for a semi-major restructuring of SlickShopper. My 1.0 had hard-coded sizes everywhere, but I wanted to support screen rotation, so something had to change. I was in the process of rebuilding things using Interface Builder when I made some key discoveries about UINavigationControllers (they have a toolbar property) and UITableViewControllers (they inherently support resizing due to rotation). I bailed on the IB stuff, and started over using table view controllers exclusively. Version 1.5 onward contains table view controllers exclusively. I patted myself on the back for so deftly having avoided IB.

 

Today, I'm long past my fear of IB, and I've mostly given up on pure table view controllers. I use IB as much as possible, and prefer to set everything up as a plain view controller. I enjoy the flexibility this provides. (Tip: Don't name your view controllers as SomethingTableViewController, because the 'Table' part becomes incorrect if you change your mind about implementation later) I encounter a number of people struggling with basic aspects of table views, so I'm going to pool together the techniques I've learned from books and Apple's sample programs.

 

This will be the first installment of several posts devoted to the creation of plain view controllers that feature a table view. Today I'm going to focus on basic setup of the view controller. Future installments will look at using IB-based table cells, how to implement a search bar, and any other useful things I think of along the way. Please feel free to post requests in the comments area. My intention is to be as step-by-step as necessary, but I will also attempt to include a sample project at the end of each post.

 

Initial Setup

 

I'm not really going to go into the various places this view controller could be used. Theoretically, it should be perfectly usable in a view-based app, a navigation-based app, tab-based app, etc. So, create a new project using whatever template you like. I'm going to use the navigation-based template, as I intend to show how to pass data to a sub-controller at some point in this series. But feel free to use whatever template you want, as the choice doesn't really impact what I'm going to do here.

 

After the project has been set up, create a new file.

 

 

Choose a UIViewController subclass, and hit the toggle to indicate that you want to use a XIB file.

 

 

I'm calling mine SampleViewController, you can call it whatever you want. The XIB checkbox is relatively new, so if you don't have it, simply create your own XIB file, and give it the same name. And then start downloading the newest version of Xcode.

 

We need to do work in all 3 files that were just created, but in order to avoid bouncing around I'm going to work in this order: .h -> .xib -> .m.

 

Prepare The Header

 

Your .h file should look like this:

 

#import <UIKit/UIKit.h>

@interface SampleViewController : UIViewController
{
}

@end

 

Since this is a plain view controller, we need to add some information to it in order to work with a table view. The parts that we are about to add are included for free with table view controllers, hence the appeal. But it is easy enough to add manually, so here we go.

 

First, we need to adopt a couple of protocols. Table views are designed to be generic, and rely on other objects to provide customization. For our immediate use, customization mostly refers to providing content, but it can also apply to appearance. The necessary protocols are UITableViewDataSource, and UITableViewDelegate. Data source, as the name implies, provides the content. The delegate pattern is used throughout Cocoa, and indicates that one object will be doing work on behalf of another object. There are two protocols because they serve different needs, and if desired they could be distinct objects. We are already in a class that is perfectly capable of doing the work, but if you had a reason to do so, you could certainly make two more external classes to accomplish the same thing. For simplicity, generally speaking you'll just use your view controller. To indicate that your view controller can serve in these roles, add this to the .h file:

 

#import <UIKit/UIKit.h>

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
{
}

@end

 

There are no required methods in UITableViewDelegate, but there are in UITableViewDataSource. So, if you build your project before we finish up, you will get some warning messages related to the absence of those required methods. You can ignore the warnings for now, but by the time we finish up, make sure the warnings are gone.

 

We are going to graphically place a table view into our main view in Interface Builder, just as if we were placing a button or a label. We will have a reason to talk to that table view object. The way to establish that line of communication is to declare IBOutlets here in the .h file. We need to declare a table view property, like this:

 

#import <UIKit/UIKit.h>

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
{
}

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

@end

 

We're basically done now, but before we move on, let's take care of the structure that will hold our contents. I'm just going to use an array for now, as arrays work quite well with the way table views expect to receive information.

 

#import 

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
{
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) NSMutableArray *contentsList;

@end

 

Lay Out The Interface

 

Open up the XIB file. We want the view to be capable of supporting landscape, so we need to change the resizing masks. Select the view, then hit Cmd-3 to bring up the size inspector. Toggle the masks as shown here so that the view is fully flexible, and go ahead and make the height 480 for good measure.

 

 

If you are unable to make these changes, hit Cmd-1, turn off any simulated UI elements like the status bar, then come back and try again. (Disclosure: I only figured that out just now while typing this up... I thought it was a bug. I've been deleting and re-creating the view for quite some time)

 

Drag a table view from the palette onto the view. It should automatically expand to fill the entire view.

 

 

The resizing masks should already be set, but go ahead and verify them for the table view just in case.

 

 

In order to talk to the table view, we need to use the IBOutlet that we declared in the .h. To do that, Right-click (Ctrl-click) on File's Owner, and drag to the table view. (I've resized the view for sake of screen capture here)

 

 

You should see the table view highlight, and you should see "Table View" appear in a little box at the lower right, thus confirming your selection. When you let go, a window will appear:

 

 

Select the name of the IBOutlet that was created in the .h file.

 

We're not quite done yet. Remember the data source and delegate from the .h file? That declaration simply published the fact that our view controller is willing to serve that role. But that alone does not mean that the table view knows who to talk to. There could be any number of conforming classes eligible, so we need to identify specifically which class(es) this table view will use.

 

Select the table view, and hit Cmd-2. At the top of the inspector are outlets for the delegate and dataSource. Select the circle next to each one, and drag to File's Owner (regular left-click drag).

 

 

 

So, the view controller is able to talk to the table view, the table view will request information from the view controller, and the whole thing is resizable. Thus concludes our trip to IB.

 

Implement

 

Now for the hard part. Most of what we will be doing in the .m file would be exactly the same or very similar if we were using a table view controller instead. Let's start at the top and work our way down.

First, synthesize the properties:

 

#import "SampleViewController.h"

@implementation SampleViewController

@synthesize mainTableView = ivMainTableView;
@synthesize contentsList = ivContentsList;

 

Most examples show dealloc at the bottom, but I prefer to have it up top so I can quickly glance at the properties. Follow memory management rules and release the properties.

 

- (void)dealloc
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);

   [self setMainTableView:nil];
   [self setContentsList:nil];

   [super dealloc];

   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
}

 

Somewhere we need to build the data that will appear in the table view. For a simple case like this, viewDidLoad will work just fine.

 

- (void)viewDidLoad
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);

   [super viewDidLoad];

   NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"Red", @"Blue", @"Green", @"Black", @"Purple", nil];
   [self setContentsList:array];
   [array release], array = nil;

   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
}

 

Build an array, stick it into the property, then release it.

 

For this example, we aren't too worried about the displayed contents being incorrect. But in a real app, if activity in another view controller could cause the contents here to change, we want to make sure that the user sees the updated information. To make the table refresh every time the view is displayed, we'll use viewWillAppear.

 

- (void)viewWillAppear:(BOOL)animated
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);

   [super viewWillAppear:animated];

   [[self mainTableView] reloadData];

   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
}

 

I mentioned before that UITableViewDataSource has some required methods, so let's get those out of the way. First, the table is going to ask the data source how many rows are involved. The answer should be based on our array, and we use this method to respond to the table's question:

 

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);

   NSInteger rows = [[self contentsList] count];

   NSLog(@"rows is: %d", rows);
   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
   return rows;
}

 

Note the section variable that we aren't using here. So far, we only have one section, so there isn't a need to worry about it. In a future post, I'll show how to do a multi-section table, at which point this method gets a bit more involved.

 

Next, the table is going to ask for a view to display in each row. Apple has provided a UIView subclass called UITableViewCell that is pre-configured for many common table needs. You create a cell, give it some content, and then give that cell to the table. Repeat as needed for each row. A lot of explanation is going to be needed here, so let's jump to the end result, and I'll discuss afterwards...

 

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);

   NSString *contentForThisRow = [[self contentsList] objectAtIndex:[indexPath row]];

   static NSString *CellIdentifier = @"CellIdentifier";

   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
   if (cell == nil)
   {
      cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
      // Do anything that should be the same on EACH cell here. Add subviews, fonts, colors, etc.
   }

   // Do anything that COULD be different on each cell here. Text, images, etc.
   [[cell textLabel] setText:contentForThisRow];

   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
   return cell;
}

 

In summary:

  • Grab the object that has (or in this case, is) the information we want to display for this row
  • Ask the table view if any cells are available for recycling.
  • If not, create a new cell
  • Specify the content for the cell

I'll talk more about cell customization at a later date, but for now please note the comments I added regarding where you should customize different elements of the cell.

First, a little bit about the NSIndexPath. If you were trying to describe the location of a point on a grid, you would most likely use (X,Y) coordinates. The index path provides a way of describing a location within the table, but instead of (X,Y) coordinates, it is using (section, row) coordinates. These are numbered in the exact same way an array is, so the first item is 0, the second item is 1, and so on. We only have one section, so we will only be dealing with section 0 for now. Within our only section, we are providing five pieces of information, so we'll be talking about row 0, row 1,....up to row 4.

 

So this method begins with the table asking the question "Hey, I'm now at the first section and the second row... what should I show here?" We need to figure out which row is being requested, and we do that by asking the indexPath for its row value. [indexPath row] (later we'll do the same thing to get a section value). Assuming we are building the table up from scratch, we should be dealing with the first row, so row 0. Now I know which piece of information I want from the array: the first item, so the item at index 0. For convenience I assign that to a local variable.

Next I declare a string variable. I don't actually know what "static" technically means, other than the obviously implied "this does not change". I suppose you could #define a constant instead. The purpose of this string will be to allow the table view to identify cells in a queue that it will create.

 

The table's cell queue exists because flinging your way through a list of data needs to happen as fast as possible, but building a cell from scratch can be expensive from a performance standpoint. If we were providing 1000 strings for this table, we don't want to actually create 1000 cells. We only need to create enough to cover the visible screen, plus a couple extra for buffer, and then we can reuse those cells over and over again. As a cell slides off the top of the screen, it goes into the queue, only to reappear at the bottom of the screen with new content. We don't want to build new cells if it can be avoided.

Thus, the next thing we do is ask the table if any cells are available for reuse, here:

 

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

 

We use the string identifier, because there could be multiple kinds of cells being stored, so we want to make sure we get the right kind. If a cell is available, it will be provided to the cell variable. However, if there aren't any cells available (as would be the case when starting from scratch), the return from this call is nil. So, we find out if we actually have a cell right now:

 

if (cell == nil)
{
   cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
   // Do anything that should be the same on EACH cell here. Fonts, colors, etc.
}

 

If there is a cell, this part doesn't happen. If there isn't a cell, this part will build a new one.

Lastly, we provide our content:

 

// Do anything that COULD be different on each cell here.  Text, images, etc.
[[cell textLabel] setText:contentForThisRow];

 

Standard cells have a label property, and I'm setting the text using our content string. I'm going to again emphasize the comments I've put in there. When you get to this point of the method, you have 2 possibilities: 1) The cell is brand new, or 2) The cell has been recycled. Recycled cells will most likely still have their old content, so you cannot make assumptions about the state of the cell you are working with. It could be pristine, it could be dirty. So this area of the method MUST make sure the end result is correct. If you alternate font colors between red and green, then you should have an if/else statement here that makes the font red OR makes the font green. You cannot assuming the incoming color is correct.

 

That covers the required methods for the data source. There are many, many other optional methods - from dataSource and from delegate - for performing a variety of tasks, but for now I just want to draw attention to one of them.

 

If you build-and-run your app, you should see the list of colors in your table. If you tap a row, it will stay highlighted. Let's turn that off. This method is how the table says "Hey, I was touched here... what do you want me to do about it?"

 

- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);

   [tableView deselectRowAtIndexPath:indexPath animated:YES];

   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
}

 

We will do a lot more with this method later, but for now we'll simply deselect the row.

And thus concludes this edition of table talk. Tune in, uh... later... for the next installment. In the meantime, here is the sample project for this stage of the exercise.

TableViewTutorial_Part1.zip