Showing posts with label UITableViewController. Show all posts
Showing posts with label UITableViewController. Show all posts

August 3, 2009

Assigning Delegates

Kind reader Demitri Muna pointed out a flaw in my TableFormEditor package. I stumbled into a trap I fear many newbie Objective-C programmers also fall into: that of retaining delegates. It is a two-line change, but I feel this issue is important enough to write about it.

The basic problem is a retain cycle which results in a memory leak. This has been fairly well documented in this thread, but I will go into more detail here in the context of the TableFormEditor.

In this context, there are 2 actors involved:

  1. the client object
  2. the TableFormEditor object

When the client object creates a TableFormEditor instance, it needs to pass a delegate to handle the various callbacks. Typically, but not required, the delegate is the client object (i.e. self).

The TableFormEditor has this declaration in its .h file:

@property (nonatomic,retain) id <TableFormEditorDelegate> delegate; 


So what this means is that the client has a retain on the TableFormEditor (because it created it via [alloc]), and the TableFormEditor instance has a retain on the client (via the "retain" keyword in the @property declaration).

This is the dreaded A -> B and B -> A scenario described in the Stack Overflow thread mentioned above. When you are in this situation, A and B will never go away because B has a retain on A that will not release until B goes away. But B won't go away until A goes away. Result: memory leak!

However, all is not lost if you followed the pattern I have in the TableFormEditor example. In the example, I do not keep the TableFormEditor object around. As soon as I push it on the Navigation Controller's stack, I release it:

   
// stick the editor onto the navigator's stack 
[self.navigationController pushViewController:form animated:YES];      

// release the form since the navigator retained it 
[form release]; 


But, if you retain the pointer to the TableFormEditor, like keeping it in an instance variable, then you will likely end up with a memory leak.

Thus I have released version 1.6 of the TableFormEditor class to make this change:

@property (nonatomic,assign) id <TableFormEditorDelegate> delegate; 


This simply assigns the pointer without bumping up the retain count. This breaks the cycle and the potential for memory leaks.

For more information on retain cycles and how to prevent them, I urge you to check out this blog article at Cocoa With Love.





July 6, 2009

Changing the Accessory View

In my application, I want to replace the button in the accessory view area of a table cell with my own button. I want it to look like this:
Clicking on the cell should produce the callback tableView:(UITableView *) aTableView didSelectRowAtIndexPath:(NSIndexPath *) indexPath. However, when the pencil icon is hit, I want it to do a different callback. I didn't want to make a custom table cell class, and from reading the documentation, it appeared relatively straight forward to use the existing table cell class. All I had to do was set the cell property accessoryView to something else. Here is what the SDK documentation says:
@property(nonatomic, retain) UIView *accessoryView Discussion If the value of this property is not nil, the UITableViewCell class uses the given view for the accessory view and ignores the value of the accessoryType property. The provided accessory view can be a framework-provided control or label or a custom view. The accessory view appears in the the right side of the cell. If the cell is enabled (through the UIView property userInteractionEnabled) , the accessory view tracks touches and, if tapped, sends the accessory action message set through the accessoryAction property.
Ok, sounds simple enough, so I went ahead and coded up what I thought was a trivial solution. I picked a UIImageView to put into the accessoryView. I was careful about setting the userInteractionEnabled flag and everything. Here is the snippet of code I came up with (which belongs in the cellForRowAtIndexPath method, in the case we have to create a new UITableCell object):
// stick the pencil icon on the accessory view
UIImageView* pencil = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon-pencil.gif"]];
pencil.userInteractionEnabled = YES;
pencil.frame = CGRectMake(0, 0, 15, 15);
cell.accessoryView = pencil;
cell.accessoryAction = @selector(didTapEditButton:); 
Well, this didn't work. All taps on the image produced the didSelectRowAtIndexPath callback. I re-read the documentation on accessoryView. That last sentence mentions the accessoryAction property, so I thought I should take a peek at that:
@property(nonatomic) SEL accessoryAction Discussion If you specify a selector for the accessory action, a message is sent only if the accessory view is a detail disclosure button—that is, the cell's accessoryType property is assigned a value ofUITableViewCellAccessoryDetailDisclosureButton. If the value of this property is NULL, no action message is sent. The accessory view is a UITableViewCell-defined control, framework control, or custom control on the right side of the cell. It is often used to display a new view related to the selected cell. If the accessory view inherits from UIControl, you may set a target and action through the addTarget:action:forControlEvents: method. See accessoryView for more information.
Hmmm. That very first sentence says I can't do this with a custom view. But the next paragraph states I can just use the addTarget:action:forControlEvents: method. (Then it makes a circular reference back to accessoryView for more info, which doesn't offer more info.) But a UIImageView is not derived from UIControl, so that is not available. The solution to this mess is to use a UIButton instead of a UIImageView. UIButtons are UIControls, so the final code looks like this:
// stick the pencil icon on the accessory view
UIButton* pencil = [UIButton buttonWithType:UIButtonTypeCustom];
[pencil setImage:[UIImage imageNamed:@"icon-pencil.gif"] forState:UIControlStateNormal];
pencil.frame = CGRectMake(0, 0, 15, 15);
pencil.userInteractionEnabled = YES;
[pencil addTarget:self action:@selector(didTapEditButton:) forControlEvents:UIControlEventTouchDown];
cell.accessoryView = pencil; 
This time it works. I get the desired callback when the pencil icon is touched. The summary is: use a UIControl based widget in the accessoryView, and ignore the accessoryAction property.

May 29, 2009

Trapping the UINavigationBar Back Button (part 3)

I have covered the topic of trapping the back button in order to do something. These previous forays are part 1 and part 2. I had implemented my solution from part 2, and it is working well. However, I am always on the look out for a better, cleaner solution. Just recently, an anonymous commenter pointed out something I had not noticed before: the UINavigationBarDelegate prototype has a method called navigationBar:shouldPopItem: It is called just before the item is popped. If it returns NO, then the pop won't actually happen. But this can be the perfect way to insert some logic before the pop happens (and return YES at the end). Here is how you would implement your back-button press event logic:
- (BOOL)
navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
   //insert your back button handling logic here

   // let the pop happen
   return YES;
}  
This is so much cleaner than my previous solution. I love it! Thanks to whomever pointed out the obvious.

April 22, 2009

TableFormEditor v1.4: text field settings

A while ago, I introduced a reusable class for producing editable forms. This article can be read here. There was also a followup article containing a detailed usage example and some bug fixes in the TableFormEditor code. I have been using this class since then, and it quickly became clear that I needed a way to configure the text fields that were used in the form. For instance, one of the fields in my app takes a URL as input, and I would have liked to use the special keyboard that is designed for that type of input. This forced me to do some research on how to configure text fields. This research culminated into these two articles: part 1 and part 2. Now it is time to put this research into the TableFormEditor class. I came up with a solution that may not be the best, but since I need to make some progress on my app, I forged ahead and implemented this solution. Comments are appreciated on other ways to do this. It currently serves its purpose and might even be deemed "clever". If you are familiar with the TableFormEditor class, you know that you provide the field labels in an array. These labels are also used as keys to a dictionary that contains the field data. The edited data is also returned in a dictionary, again keyed by the row labels. Even if you don't use the field rows as labels, you still need to provide those for keying purposes. I extended this idea by allowing you to provide text field properties for each field. These properties are set to configure the behavior of the text field, as well as the keyboard behavior. All the supported properties are described in the two-part article mentioned above. I first thought about using a dictionary to hold the properties, or creating a new class, but decided to use a real UITextField object to hold the properties. After all, this class already has all the properties I want. What you do is create a UITextField object, set all the properties you need, and put this object in a dictionary. The field labels provide the dictionary keys, just as they do for the field data dictionary. It's important to understand that these UITextField objects you create are not used in the GUI; they are only used to hold values for properties, then they are thrown away. In this sense, they serve as "prototype" UITextField objects. The reason I chose to do it this way was based purely on convenience. There was no sense creating another class with all the same properties. And when setting the properties, XCode knows what the properties and it does the code completion for you. I have modified the TableFormEditor class to have an additional property: textfieldProto This is a mutable dictionary. The keys are the label names of each field. The value for each key is a UITextField object that has all the desired properties set. I have modified the example to have 4 fields:
  1. name field. For this field I would like auto correction turned off, but would like to auto capitalize the beginning of each word.
  2. age. This field is all numbers, so I would like the numeric keypad to come up when this field has the focus.
  3. homepage. For this, I want the URL keyboard, and turn off any auto capitalization and auto correction.
  4. password. This field should use the secure key feature, which dots out each letter as you type it.
The following is the code snippet from the example that demonstrates how you create the UITextField prototype objects and set them in the TableFormEditor object:

 
   // create the dictionary to hold the text field properties
   NSMutableDictionary* textFieldProto = [[NSMutableDictionary alloc] initWithCapacity:4];
  
   // prototype for the Name field
   // shrink the font to fit, don't autocorrect, autocap first letter
   UITextField* tf = [[UITextField alloc] init];
   tf.adjustsFontSizeToFitWidth = YES;
   tf.minimumFontSize = 7.0;
   tf.autocorrectionType = UITextAutocorrectionTypeNo;
   tf.autocapitalizationType = UITextAutocapitalizationTypeWords;
 
   // add it to the dictionary
   [textFieldProto setObject:tf forKey:@"Name"];
   [tf release];
  
   // prototype for the Age field
   // use the numeric keyboard
   tf = [[UITextField alloc] init];
   tf.keyboardType = UIKeyboardTypeNumberPad;
  
   [textFieldProto setObject:tf forKey:@"Age"];
   [tf release];
  
   // prototype for the Homepage field
   // use the URL keyboard, turn off autocorrect, show clear button, autocap off
   tf = [[UITextField alloc] init];
   tf.keyboardType = UIKeyboardTypeURL;
   tf.autocorrectionType = UITextAutocorrectionTypeNo;
   tf.autocapitalizationType = UITextAutocapitalizationTypeNone;
   tf.clearButtonMode = UITextFieldViewModeAlways;

   [textFieldProto setObject:tf forKey:@"Homepage"];
   [tf release];
  
   // prototype for the Password field
   // use secure entry
   tf = [[UITextField alloc] init];
   tf.secureTextEntry = YES;
  
   [textFieldProto setObject:tf forKey:@"Password"];
   [tf release];

   // now register all the text field prototypes
   form.textFieldProto = textFieldProto;
  
The updated example XCode project is available for you to play around with, as well as the updated TableFormEditor class to support text field prototypes,  using this link.

April 9, 2009

Putting a Button in a Table

Sometime you may need to add a button to the contents of a UITableViewController. While you have the ability to put buttons in the navigation bar or in a toolbar at the bottom, it is sometimes best to simply put the button "inline" near your data. Sure, you could simply put a UIButton control right in a UITableCell object, but I have found the results of that not very attractive, especially in grouped tables. So what's the best way? I like to use the Apple applications as examples of good design. There are a couple Apple applications that put UIButtons in UITableViewControllers. Take a look at these examples. This one is from the mail settings, where there is a "Delete Account" button at the bottom of the table:
And this one is when you are editing a contact, where there is another "Delete" button at the bottom:
Although they are the same size of a single cell section, you can tell these are not normal rows. The giveaways are the rounded corners that don't quite match those of the neighboring rows. My guess is these are custom buttons, which stretch a background image to fit a frame that's the same size of a cell. But using UIButton controls is not the only way to get button behavior. You can simulate a button with just a basic grouped table cell; no UIButton at all. This is what I call cell-based buttons, for lack of a better term. I found a couple Apple applications that appear to use this technique. For example, here is the the settings from Safari:
That section of three rows in the middle contain cells where each operates as a button. The phone settings also uses this, with the "Change Voicemail Password" cell in the middle. There are probably other ways to get button functionality into your tables, but these two seem to be the most common. This article will explain the advantages and disadvantages of each approach, and I'll provide some code snippets that shows how to do them.

Cell-Based Buttons

Simulating a button in a cell has one big advantage: it's super easy to do. If you don't care about it looking fancy, or want to bunch up several "buttons" in one section, as the one above for editing a contact, then a cell-based button is a good option. For simple black on white text, all you have to do is set the cell.text property to your button text. Since the visual aspects of a table cell can be customized quite extensively, you really have lots of flexibility here. However, once you go beyond the standard UITableCell object, the complexity of your code increases. I believe that once you have gone beyond the basic black on white text, you might as well go with a real button.

Implementing

How a cell-based button works is that your code simply traps the tableView:didSelectRowAtIndexPath: message. You associate the section and row with which "button" was hit and perform whatever action that button is supposed to do. For example, from the image above, you can associate that block of buttons with section #1, and row #0 is the "Clear History" button, row #1 is "Clear Cookies", etc. One thing to remember is that the action of touching a cell automatically highlights the cell. Before you do anything else, you should turn off that highlight. Here is some example code of how we might implement the above section of three buttons. There are two aspects here: setting up the buttons for display, and handling the actions when the buttons are selected. Setting up for display. All of this is fairly boilerplate code for the tableView:cellForRow: message. The big difference here is that we are not fetching the row contents from a database, but hard-coding the button title. This can easily be internationalized as well.
- (UITableViewCell *)
tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

   static NSString *CellIdentifier = @"Cell";

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

   // are we setting up our button section?
   if(indexPath.section == 1) {
       switch(indexPath.row) {
           case 0: cell.text = @"Clear History"; break;
           case 1: cell.text =@"Clear Cookies"; break;
           case 2: cell.text = @"Clear Cache"; break;
       }
   }
   else {
       // do whatever for the other sections
   }

   return cell;
}
For cell-based buttons, you want to avoid using the accessory view, like putting in a detailed disclosure button or the checkmark. Having these items on a "button" will be confusing. Handling actions. All you do here is figure out which "button" was hit and do whatever it is you need to do. Also notice the code to turn the highlight off.
- (void)
tableView:(UITableView *) aTableView didSelectRowAtIndexPath:(NSIndexPath *) indexPath
{
   // remove the row highlight
   [aTableView deselectRowAtIndexPath:indexPath animated:YES];

   // Is this section our "button" section?
   if(indexPath.section == 1) {
       switch(indexPath.row) {
           case 0: [self doClearHistory]; break;
           case 1: [self doClearCookies]; break;
           case 2: [self doClearCache]; break;
       }

       return;
   }
   else {
       // do other stuff...
   }
}

Real Buttons

Real buttons offer all the advantages you get with a real button. For one thing, the size of the button can be fully customizable. This allows you to have two buttons side by side, as in this example from showing detailed info of a contact:
You can't have two table cells side by side like that, so these have to be buttons. You'll note that these rounded corners match the rounded corners of the table cells. I think these buttons use the normal RoundedRect button, as opposed to custom buttons. Custom buttons are mostly needed when you want a background color other than white. But if all you need are plain white backgrounds, you can use the RoundedRect buttons. Which leads to the other advantage of using real buttons: better control over background colors. Setting a background color in a grouped table cell is difficult. Just simply setting the background property to a color is not enough, as the rounded corners do not get the color. The only solution I've seen to this problem is a complicated bit of code to color in the curved corners (as demonstrated here). You also don't get the cool gradients as you see in the two red buttons on the top two examples.
There are probably other advantages to using real buttons, but to me these are enough to force me to use buttons sometimes.

Implementing

So how are UIButtons integrated into a UITableView? Again, there may be several ways, but I find the easiest is to sneak the buttons into a section header. Each section can have a header view. In this view is where you put your UIButton objects. The section itself has no data rows, only a header. So what I do is accommodate for one extra section in my table, but return 0 for the number of rows.
- (NSInteger)
numberOfSectionsInTableView:(UITableView *)aTableView
{
   // return the number of sections the data is organized in, plus 1 for the button
   return 2 + 1;
}
Assuming you want your button at the end of the table, you would use code like this to return the number of rows:
- (NSInteger)
tableView:(UITableView*) aTableView numberOfRowsInSection:(NSInteger) sectionNum
{
   if(sectionNum == 3) {
       // this is our button section
       return 0;
   }
   else {
       // do whatever, according to the data
   }
}
Now we have to create our buttons. This is done in the tableView:viewForHeaderInSection:
- (UIView *)
tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)sectionNum
{
   if(sectionNum == 2) {
       // create the parent view that will hold 1 or more buttons
       UIView* v = [[UIView alloc] initWithFrame:CGRectMake(10.0, 0.0, 300.0, 44.0)];
 
       // create the button object
       UIButton* b = [UIButton buttonWithType:UIButtonTypeCustom];
       [b setBackgroundImage:[[UIImage imageNamed:@"redbutton.png"] stretchableImageWithLeftCapWidth:12.0 topCapHeight:0.0] forState:UIControlStateNormal];
       [b setBackgroundImage:[[UIImage imageNamed:@"bluebutton.png"] stretchableImageWithLeftCapWidth:12.0 topCapHeight:0.0] forState:UIControlStateHighlighted];

       b.frame = CGRectMake(10.0, 0.0, 300.0, 44.0);
       [b setTitle:@"Button Title" forState:UIControlStateNormal];

       // give it a tag in case you need it later
       b.tag = 1;

       // this sets up the callback for when the user hits the button
       [b addTarget:self action:@selector(action:) forControlEvents:UIControlEventTouchUpInside];
     
       // add the button to the parent view
       [v addSubview:b];

       return [v autorelease];
   }
   else {
       // stuff for other sections
   }
}

The values for the frames are specific to portrait mode; landscape mode will need some modifications. To add more buttons in this view, just adjust the y parameter in the frame for the button. I typically add 46.0 from the last one. This code makes use of two button images:

I got these from user "hakimny" from this thread. Creating these aren't hard. (I have posted an article on how to create simple background images like this in Inkscape.)
If you want to create a button with a white background, then you don't need to create a custom button. You can create a RoundedRect button that will look more like the surrounding cells, like you saw above. In this case, your button creation is simply:
b = [UIButton buttonWithType:UIButtonTypeRoundedRect]; 
You don't need to set the background images. If you want to create two side by side buttons, all you have to do is monkey around with the frames of the buttons so that they are side by side instead of on top of each other. There is one gotcha I have discovered using this mechanism, and I think it is an SDK bug. If your table takes up more than one screen and you have to scroll to see your button, some of the button may remain hidden under the tab bar or tool bar. You won't be able to scroll the entire header view into view. This is especially noticeable if you have 2 or more buttons stacked on top of each other. I think Cocoa Touch is confused by there being no data rows, or maybe it can't quite figure out how high the header view is. So the way around it is to tell how high your view is. Once you do this, the view is no longer partially hidden:
- (CGFloat)
tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)sectionNum
{
   return 46.0;
}

Of course, if you have more buttons in this section, you would return the appropriate height. There you have a quick tutorial on putting buttons in a table. How you choose which mechanism to use is up to you. My own guidelines are as follows:
  1. If I want a simple black on white button: use a cell-based button
  2. If I need a colored button: use a real button
  3. If I want multiple buttons per row: use real buttons

April 6, 2009

Configuring Text Fields (Part 2)

In Part 1 of this article, I described the various properties used to modify the "look and feel" of text fields. Most of these properties deal with the visual aspects, such as font and background color. They also handle various techniques for clearing out the text in a text field. In this second part, I will focus on the virtual keyboard and the behaviors of changing your entered text (due to spelling or capitalization errors). It is likely that most people will be interested in changing some of the default behaviors. As revolutionary as the iPhone virtual keyboard is, some of the things it does can be quite annoying for some applications. At the same time, some of the customizations you can make really help the usability of your application, so it is best to understand all your options in order to make the best user experience possible. These are additional properties of the UITextField object you can change: autocapitalizationType
Setting this property determines when characters are auotmatically capitalized. The allowed enumerations are:
  • UITextAutocapitalizationTypeNone
  • UITextAutocapitalizationTypeWords
  • UITextAutocapitalizationTypeSentences
  • UITextAutocapitalizationTypeAllCharacters
None will not autocapitalize anything. Words will capitalize the first character of every word, whereas Sentences will capitalize the first character of every sentence. AllCharacters will capitalize every character. The default is None.
autocorrectionType
The autocorrection mechanism on the iPhone allows you to type "fast" and it will correct most of your mistakes. I find this works well, but in some cases I don't want this behavior. Luckily you can disable it. This property can be set to one of these values:
  • UITextAutocorrectionTypeDefault
  • UITextAutocorrectionTypeNo
  • UITextAutocorrectionTypeYes
The Default defers to what the documentation refers to what the "script system" supports, with no explanation of what a script system is. I assume this indicates in what context the keyboard is used. The UITextField is not the only user of the keyboard. By setting autocorrectionType to Default, you will get the auto-correction behavior *if* your current script system supports it. For the purposes of UITextField, it does support auto-correction. Setting this property to No will disable auto-correction. The default is Default, which for UITextField views, means auto-correction is enabled.
enablesReturnKeyAutomatically
By default, the return key on the keyboard is enabled. However, you can change this to be disabled *until* some text has been entered in the text field. I'm not sure what use this is for text fields, but you might have a need. If you set this property to YES, then you will have this keyboard when you have no text entered:
The return key is disabled, but as soon as you entered some text, the keyboard changes to this:
keyboardAppearance
There are two types of keyboard appearances supported: one has the blue-gray background like the one above, and the other has a black background, like you see on alerts. The two enums this property supports are:
  • UIKeyboardAppearanceDefault
  • UIKeyboardAppearanceAlert
If you choose the alert appearance, the keyboard looks like this: As far as I can tell, there is no other difference in looks or behavior with this keyboard
keyboardType
Depending on what kind of input you are gathering in your text field, you can have different keyboards that are optimized for that type of input. The keyboardType property can be set to one of the following enums. I also show what each keyboard looks like:
UIKeyboardTypeDefault:

UIKeyboardTypeASCIICapable:

The default looks the same as this. My guess is the default can change depending on context, whereas the ASCIICapable one always look like this.

UIKeyboardTypeNumbersAndPunctuation:

This is your standard numbers and punctuation keyboard.

UIKeyboardTypeURL:
   

This keyboard is handy if you are typing in a URL. Also note that if you tap and hold on the .com button, you'll get a little pop-up window that will allow you to choose .edu, .net, .org, etc. Here is what that looks like:

UIKeyboardTypeNumberPad:

This is useful if all you need is to enter numbers.

UIKeyboardTypePhonePad:

This is similar to the NumberPad, but this one resembles a phone keypad, with the letters under the numbers and the "+*#" button.
UIKeyboardTypeNamePhonePad:

This looks like your standard ASCII keypad, but the difference is when you hit the digits button in the lower left, you get a phone-specific keypad:

UIKeyboardTypeEmailAddress:

These are slightly optimized for entering email addresses.
returnKeyType
Just the return key can be changed with different labels. Like the keyboardType property, this allows you to customize the keyboard to fit your scenario. However, many of these are very application specific and may not have much use for text fields: UIReturnKeyDefault UIReturnKeyGo UIReturnKeyGoogle UIReturnKeyJoin UIReturnKeyNext UIReturnKeyRoute UIReturnKeySearch UIReturnKeySend UIReturnKeyYahoo UIReturnKeyDone UIReturnKeyEmergencyCall I won't show what all these keyboards look like. The only difference is the label on the return key. The useful ones for most text fields would be Next and Done. When the return key is tapped, no matter what text is on it, the text field will call the textFieldShouldReturn: method on its delegate. You can use this callback as a means to do something when the return is hit. One common trick is have textFieldShouldReturn: resign the first responder, or in normal English: give up the focus, which in turn hides the keyboard.
secureTextEntry
If your text field is for entering passwords or other sensitive information, you can get the password-like entering behavior by setting this property to YES. When this is YES, only the last character entered will display in the text field; all others are replaced with a dot. Here is what is looks like:
There you have all the properties you can change to modify the keyboard and auto-correction behaviors of your text fields. Instead of accepting the defaults, you can customize your text fields and make them more user-friendly and efficient.

March 23, 2009

TableFormEditor Example

I have been asked to provide a more concrete example on using the TableFormEditor. I will provide an Xcode project with a little app that demonstrates the TableFormEditor in action. The project can be downloaded from here. What this example does is display three fields of data and a button. When the button is pressed, the user either is creating a new record or editing an existing record. This example also has a new version of the TableFormEditor which addresses a couple bugs. One bug was that if you have a translucent navigation bar, then the editor puts the first field under the nav bar. The other bug is that the alert that pops up when you hit the delete button assumes you have a tab bar. If you don't it crashes. So this version fixes that assumption.

March 6, 2009

A General Purpose Table Form Editor

Note: You may have come here from another link. Be aware that I have updated this package and can read about it here. Sometimes your iPhone applications need the ability to enter or edit data on a form. For example, here is a screen from the contacts app when adding a new contact:
If you have ever coded up the UITableViewController class to do this sort of the form editing, you know that this coding task is very tedious and time-consuming. Wouldn't it be nice if there was a general purpose, reusable class for doing this? Here I present such a class.

Features

The TableFormEditor class is intended to be used within a UINavigationController. In other words, you allocate a TableFormEditor instance, configure it, then push it on top of the navigation stack. The TableFormEditor will create a grouped table and the first section will contain rows with UITextField objects to display and allow editing of data. These rows can optionally contain labels that identify what each row represents, or you can have no labels as in the above example. Optionally, you can configure a delete button to appear at the bottom of the form. This is intended to allow the user to delete the record. The text on the button is configurable. The action of the delete button will ask for confirmation before continuing.

Basic Usage

Here is how the TableFormEditor works:
  1. You allocate and initialize a TableFormEditor object.
  2. You configure various other aspects of the object, such a your fields names and what your data is.
  3. Then you push the object onto the navigation controller stack. At this point, the TableFormEditor is in control.
  4. You can bounce around the fields and edit to your heart's content. When you are done, you hit the "save" button. This will pop the controller off the navigation stack and send a message to the delegate you provide, passing the new data. It is up to your delegate to do something with this data.
  5. If you decided to hit "cancel" instead, the controller is popped off the navigation stack and a message is sent to the delegate.
  6. If you have a delete button enabled, and this button was selected, then the controller is popped off the stack and a different message is sent to the delegate.
Each of these steps is discussed in greater detail below.

Initialize

There are two schools of thought concerning initializers. Some like to force the user to pass all required parameters, often resulting in very long initializer statements. The other side prefers simple initializers, and the remainder of the parameters can be set as properties. The benefit of the former is that there will be no forgetting to set a required parameter. However this approach is not very flexible; any new additions or changes to the class often require the initializer to be changed. Very specialized initializers create fragile code. Therefore I have opted for the approach of simple initializers and setting properties. True, some properties must be set, otherwise the resulting code will be next to useless. However, I will document clearly which properties are mandatory and which are optional. There are two modes the TableFormEditor can be in: edit mode or add mode. In edit mode, you are editing/viewing existing data. In add mode you are adding a new record. What mode you're using has subtle differences in the default behavior. For instance, in add mode there isn't a delete button displayed, since there is no record yet to delete. To initialize for add mode, use the initForAdd: method:
TableFormEditor* add = [[TableFormEditor alloc] initForAdd:self];
All you provide is the delegate, typically self. The delegate must conform to the protocol TableFormEditorDelegate:
@protocol TableFormEditorDelegate
@optional
- (void) formDidCancel;
- (void) formDidSave:(NSMutableDictionary*) newData;
- (void) formDidDelete;
@end
All these methods are optional, but obviously without the formDidSave: you don't have very functional software. To initialize for edit more, use the initForEdit: method.
TableFormEditor* edit = [[TableFormEditor alloc] initForEdit:self];
Just as with the add mode, you pass in a delegate to handle the same protocol.

Configure

Now we'll discuss all the various properties you can and must set to configure the form editor. Note that even though some of these are labelled as "required", that doesn't mean your program will crash without them. But you may not see very interesting results.

Required

NSArray* fieldNames
This is a list of NSString objects which are the field names. Each field equates to a row in the form. This list serves several purposes:
  1. It determines how many rows to create in the table,
  2. It provides the labels to preface each row with,
  3. It provides the order to present the fields in the form, and
  4. It provides the key names to be used in the data dictionary.
Note that the data you provide in the data dictionary (described next) must use keys that match these names.
NSMutableDictionary* fieldData
This is the data. Depending on the mode the object was initialized with, this data has different purposes. If initialized in edit mode, then this data represents the current data. The keys are the fieldNames given above. The values are the data. Both of these are assumed to be NSString objects. If the form editor was initialized in add mode, then this data represents the placeholder text to put in each row. The example above shows placeholder text on each row. This text gives the user a hint what's to go in that field, and it disappears as soon as you start entering text. If you don't want placeholders, don't set the data. This dictionary is copied (deep copy), because TableFormEditor may modify the values.

Optional

id <TableFormEditorDelegate> delegate
This is the delegate to handle the callbacks when the user wants to exit the form. This is set up in the initializer, but if you ever need to change it, you can via this property.
BOOL showLabels
When the form rows are displayed, by default each row has the field name displayed on the left as a label. This may be overkill, especially if you use placeholders, so you can turn off the labels by setting this property to NO.
BOOL allowDelete
In edit mode, by default, this is set to YES. When YES, a delete button will be displayed after the form. Set to NO if you don't want to see the delete button. If you are in add mode, and for some reason want the delete button, you will have to force it by setting the property to YES.
NSString* deleteButtonLabel
If a delete button is displayed, this is the text used. If unset, it will use "Delete".
NSString* saveButtonLabel
The TableFormEditor puts a button in the right position of the nav bar. This button is for saving your changes and exiting the form. By default, the button text is "Save", but this can be modified with this property.
NSString* cancelButtonLabel
The TableFormEditor also puts a button in the left position of the nav bar. This is for canceling any edits made and exit the form. By default, the button text is "Cancel". Modify with this property.
NSInteger firstFocusRow
By default, the form editor will place the focus (first responder) on the first row (row 0). This action brings up the keyboard automatically. If you wish to have the focus placed somewhere else first, set this property to the row you want. If you don't want any row given focus (and thus no keyboard will come up automatically), then set this property to -1.
NSString* title
This is the title string placed in the middle of the nav bar. Default value is blank.

Future

In future releases of TableFormEditor, I will be adding more configuration options, like options to configure the keyboard and how the text fields behave.

Download

The source code to the TableFormEditor class can be freely downloaded here: version 1.0 . This software is covered under the MIT License.

February 26, 2009

Radio-button behavior for table rows

One thing missing from the iPhones UI kit is a plain old radio button. There are some controls that kind of give you the exclusive selection behavior, like a tab bar or a segment control, but there is nothing like a typical RadioButton widget. The closest you can probably get using the standard UIKit framework is a table. Tables have rudimentary support for radio button behavior. When using the standard UITableCell class, it has provisions for setting the accessory type of a row to a checkmark. You have probably seen this technique in the calendar app while editing an event alert. Your choices are presented like this:
Unfortunately, this is pure visual asthetics; you still have to manage the exclusive behavior in your code. The document "Table View Programming Guide for iPhone OS " has a good example of how to implement both exclusive and inclusive selections in chapter 7 that makes use of the checkmark accessory. I won't repeat that code here, but in a nutshell, this example code handles the message tableView:didSelectRowAtIndexPath:, and what it does (for the exclusive selection behavior) is find the previously selected cell, clear it's accessory checkmark, and put the checkmark on the current cell. It's a little cumbersome, but it works. This is all fine, but what if you didn't want to use the checkmark accessory? I have an application where I need to use the disclosure button accessory, so I needed to implement the checkmark somewhere else. The solution I chose was to create my own checkmark icon and place that in the image portion of the standard cell, over on the left hand side of the cell. I quickly discovered, though, that you can't use the same technique as in the above document. If you change the image of a cell, the changes will not be seen. For some reason, changing the accessory type of a cell is rendered immediately, but not so for the image. The only way I could get the image change to be reflected is to call reloadData: on the table view. After I did that, everything worked. However, I took this opportunity to clean up the clunky code from the example. Instead of finding my old cell, all I do in tableView:didSelectRowAtIndexPath: is set the current active row in our data and call reloadData:. The real work was moved over to the table:cellForRowAtIndexPath: method, which sets the image for each cell according to the active row. First we must handle the row selection. The instance variable activeRow is simply an integer of the currently selected row.
- (void)
tableView:(UITableView *) aTableView didSelectRowAtIndexPath:(NSIndexPath *) indexPath
{
 [aTableView deselectRowAtIndexPath:indexPath animated:NO];

 if(indexPath.row != activeRow) {
    // reset the active account
    activeRow = indexPath.row;

    // tell the table to rebuild itself
    [aTableView reloadData];
 }
}
Next we handle the actual image setting:
- (UITableViewCell *)
tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
 UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:@"Cell"];

 if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Cell"];

    // set up the cell properties that are the same for all cells on this table
    cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
 }

 // get our text for the row
 cell.text = [self.data forRow:indexPath.row];

 if(indexPath.row == activeRow) {
    cell.image = checkedImage;
 }
 else {
    cell.image = uncheckedImage;
 }

 return cell;
}
After we get a cell to work with, I compare the row with the active row. If it is a match, I use the checked image. If not I use the unchecked image. Here is the final output: