预计阅读本页时间:-
Chapter 5
Using Table Views
WHAT YOU LEARN IN THIS CHAPTER:
- Adding a UITableView
- Creating a data source
- Editing data in a UITableView
- Presenting modal views
- Using segues
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
You can find the wrox.com code downloads for this chapter at www.wrox.com/go/begiosprogramming on the Download Code tab. The code is in the chapter 05 download and individually named according to the names throughout the chapter.
In this chapter you learn how to use the UITableView. The UITableView is a little different from what you would expect a table to be. They consist of a scroll view with single-row cells instead of a table with multiple columns and rows. A better way to think of them is a scrolling list of cells.
The UITableView is probably the most used view in iOS applications. This is because of their versatility. You can use a UITableView and basic UITableViewCells for a standard look and feel, or you can use a custom UITableViewCell with varying cell heights and content for more complex user interfaces.
An example of a UITableView using basic UITableViewCells is Apple’s Settings app. You can see a more complex example in popular apps such as Facebook and Twitter.
Apple has spent a great deal of time thinking through its implementation, so UITableViews can be used in applications that need to display large data models but need only the visible data in memory. This makes them scroll and animate smoothly while loading and unloading the model objects.
For the Bands app, you can implement a UITableView using basic UITableViewCells to display all the bands stored in the application. It will be sectioned by the first letter of the band name and show those letters as section headers and in the section index. You continue with the Bands project to enable adding new bands, while also using the UITableView to display the bands you add.
EXPLORING TABLE VIEWS
UITableViews have their own delegate, the UITableViewDelegate, to control their appearance similar to the UITextViewDelegate you worked with in Chapter 4. They also have a data source protocol, the UITableViewDataSource, which interacts with the data model of the app. Because UITableViews are so prevalent in iOS applications, Apple supplies a UITableViewController class, a subclass of UIViewController, which implements the UITableViewDelegate and UITableViewDataSource along with an IBOutlet to the UITableView. You don’t need to use a UITableViewController, but it’s much easier than adding all those to another class.
Learning About Tables
The best way to learn how a UITableView works is to start adding one to your Bands app. Though you can use just a UITableView as the main view of the app, it’s more common to use a UINavigationController. This is known as a Master-Detail application. UINavigationController is a container controller that enables different UIViewControllers to display within it. It also adds a UINavigationItem at the top of the screen. The UINavigationItem has the title of the view that displays and some built-in buttons to facilitate navigation or to interact with the current view. You learn more about the navigation aspects of the UINavigationController in the Modifying Data section of this chapter.
TRY IT OUT: Adding a UITableView
- Open the Bands project in Xcode.
- Select the Main.storyboard from the Project Navigator.
- Find and drag a new Navigation Controller from the Object library onto the Storyboard, as shown in Figure 5-1.
FIGURE 5-1
- Move the arrow pointing to the left side of the View Controller and point it at the Navigation Controller.
- Select the Navigation Item of the Bands List Table View Controller in the Storyboard hierarchy, as shown in Figure 5-2.
FIGURE 5-2
- In the Attributes Inspector, change the Title from Root View Controller to Bands.
- Run the app in the iPhone 4-inch simulator. You can now see the table view, as shown in Figure 5-3.
FIGURE 5-3
How It Works
When you add a Navigation Controller from the Object library to a Storyboard, it has a UITableView set as its root UIViewController by default. The Storyboard relationship with the two dots and a line signifies this. The arrow that you moved from the View Controller to the Navigation Controller tells the Storyboard which scene to initially show when the app launches. By pointing it at the Navigation Controller, the UITableView is now shown on launch instead of the View Controller.
Now that you have the UITableView added to the Storyboard, you need to add the UITableViewController class. When you initially created the project, the View Controller already had its ViewController class in the project and was connected to the UIView in the Storyboard. For the UITableView you need to do this manually.
TRY IT OUT: Adding a UITableViewController
- From the Xcode menu select File ⇒ New ⇒ File.
- Select the Objective-C class from the dialog, and click Next.
- Name the Class WBABandsListTableViewController and set its Subclass to UITableViewController, as shown in Figure 5-4.
FIGURE 5-4
- In the next dialog, select the Bands directory where the other class files of the project are located to keep all the class files of the project together.
- Select the Main.storyboard from the Project Navigator.
- Select the Table View Controller from the Storyboard hierarchy.
- Select the Identity Inspector in the Utilities pane.
- Set the Class for the Table View Controller to the WBABandsListTableViewController class you just created.
- Control-drag from the UITableView in the Storyboard to the Bands List Table View Controller in the Storyboard hierarchy, and set it as the dataSource.
- Control-drag again from the UITableView to the Bands List Table View Controller and set it as the delegate.
How It Works
When you add a new class to your project in Xcode, you have the opportunity to set what it’s a subclass of. When you add the WBABand class in Chapter 4, “Creating a User Input Form,” you created it as a subclass of NSObject. In this Try It Out you add a new class that is a subclass of UITableViewController. This means your WBABandsListTableViewController has an IBOutlet to a UITableView and implements the UITableViewDelegate and UITableViewDataSource protocols. For Xcode to know which class to associate with the UITableView in the Storyboard, you need to change its Identity to the class you created. Finally, you connect the dataSource and delegate of the UITableView to the WBABandsListTableViewContoller class. Now when the app runs, it calls your delegate to get the information it needs to display and control the UITableView.
Learning About Cells
The UITableViewCell object represents a cell in a UITableView. Unlike the UITableView, though, they do not have a delegate, and if you use a predefined style for your cell, you won’t need to add a code file. Predefined cells are versatile, so you should consider using them first before creating a custom cell.
There are four predefined cell styles you can use: basic, left detail, right detail, and subtitle. All the predefined cells have a textLabel that is a UILabel and an accessoryView that is a UIView. The right detail, left detail, and subtitle styles add a UILabel named detailsTextLabel. The basic, right detail, and subtitles also include a UIImageView named imageView.
The basic style cell, as shown in Figure 5-5, displays the imageView on the left. The textLabel is black text left-aligned with the cell, whereas the accessoryView is aligned on the far right. The accessoryView can be set using a UIView you define or one of the standard types. In the following figures, the accessoryView uses the standard checkmark type.
The right detail style cell, as shown in Figure 5-6, is the same as the basic style cell but shows the detailsTextLabel as gray text right-aligned next to the accessoryView.
The subtitle style cell, as shown in Figure 5-7, is similar to the basic and right detail styles except it moves the detailsTextLabel under the textLabel and is black text.
The left detail style cell, as shown in Figure 5-8, is different than the other three. It does not have an imageView. It also shows the textLabel with blue text that is right-aligned to the first third of the cell with the details label left-aligned next to it with black text. (Unfortunately, you won’t notice the color difference on a figure in a black-and-white book, but you get the idea.)
For the Bands app you use a basic cell style.
Apple spent a good amount of time making sure that cells scroll smoothly. One of the features it added to accomplish this is cell reuse. This means the system keeps only a handful of cells in memory and simply changes the data presented instead of creating and deallocating new cells each time the user scrolls the table.
A UITableView uses methods of the UITableViewDataSource protocol to know how many sections are in the table, how many rows are in each section as well as getting the actual UITableViewCells to display. You will implement these methods in the WBATableViewController class in the next Try It Out.
TRY IT OUT: Displaying a UITableViewCell
- Select the Main.storyboard from the Project Navigator.
- Select the Table View Cell from the Storyboard hierarchy.
- Set the Style of the cell to Basic in the Attributes Inspector.
- Set the Identifier to Cell.
- Select the WBABandsListTableViewController.m file from the Project Navigator.
- Find the numberOfSectionsInTableView: method, and change the return value to 1, as shown in the following code:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
} - Find the tableView:numberOfRowsInSection: method, and change the return value to 10, as shown in the following code:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return 10;
} - Find the tableView:cellForRowAtIndexPath: method, and add the following code:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:CellIdentifier
forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = [NSString stringWithFormat:@"%d", indexPath.row];
return cell;
} - Run the app in the iPhone 4-inch simulator. You can see 10 numbered cells (0–9) in the table, as shown in Figure 5-9.
FIGURE 5-9
How It Works
The UITableView in the Storyboard has a prototype cell associated with it. The first thing you do is set its style to Basic and its identifier to Cell. Next you modify three of the UITableViewDataSource protocol methods. The first is the numberOfSectionsInTableView: method, which tells the UITableView there is one section. The second, tableView:numberOfRowsInSection:, tells the UITableView there are 10 rows in that section. The last is the tableView:cellForRowAtIndexPath: method, which dequeues a UITableViewCell with an identifier of “Cell” if one exists, or creates a new one. The code then sets the textLabel to the row number of the indexPath. A NSIndexPath simply has the section number and row number of the cell the table is going to present.
NOTE If you run the app and the UITableView does not display the cells correctly, make sure you have the Identity of the table set to the WBABandsListTableViewController class and that the dataSource and delegate are connected correctly.
IMPLEMENTING THE BANDS DATA SOURCE
In the last chapter you added a single WBABand data model object and used NSUserDefaults to store it. In this section you expand on that by storing as many WBABand objects as the user wants to add to the app. You also need to keep in mind how the bands will display in the UITableView. This way you can use the storage as the data source for the table.
Creating the Band Storage
The easiest storage option to use to support sections is an NSMutableDictionary. As described in Chapter 2, “Introduction to Objective-C,” an NSMutableDictionary is a key/value data storage object. For the Bands storage, the first letter of the bands is the key, and the value is an NSMutableArray with all the bands that have that first letter.
Because you section the bands by the first letters of their names, it also makes sense to sort them in alphabetical order. To do this you need a way to compare two bands by their first names. All subclasses of NSObject have a compare: method. You need to override this method in the WBABand class to compare bands by their names.
Finally, you also need to implement another NSMutableArray of first letters used. You learn why in the Implementing Sections and Index section of this chapter, but it makes sense to implement the code for it now while adding the code for the WBABand data storage in the following Try It Out.
TRY IT OUT: Adding Band Object Storage
- Select the WBABand.m file from the Project Navigator, and add the following code to the implementation:
- (NSComparisonResult)compare:(WBABand *)otherObject
{
return [self.name compare:otherObject.name];
} - Select the WBABandsListTableViewController.h file from the Project Navigator, and add the following code:
@class WBABand;
@interface WBABandsListTableViewController : UITableViewController
@property (nonatomic, strong) NSMutableDictionary *bandsDictionary;
@property (nonatomic, strong) NSMutableArray *firstLettersArray;
- (void)addNewBand:(WBABand *)WBABand;
- (void)saveBandsDictionary;
- (void)loadBandsDictionary;
@end - Select the WBABandsListTableViewController.m file from the Project Navigator.
- Add the WBABand.h file to the imports with the following code:
#import "WBABand.h"
- Add the following code before the implementation:
static NSString *bandsDictionarytKey = @"BABandsDictionarytKey";
- Add the following code to the implementation:
- (void)addNewBand:(WBABand *)bandObject
{
NSString *bandNameFirstLetter = [bandObject.name substringToIndex:1];
NSMutableArray *bandsForLetter = [self.bandsDictionary
objectForKey:bandNameFirstLetter];
if(!bandsForLetter)
bandsForLetter = [NSMutableArray array];
[bandsForLetter addObject:bandObject];
[bandsForLetter sortUsingSelector:@selector(compare:)];
[self.bandsDictionary setObject:bandsForLetter forKey:bandNameFirstLetter];
if(![self.firstLettersArray containsObject:bandNameFirstLetter])
{
[self.firstLettersArray addObject:bandNameFirstLetter];
[self.firstLettersArray sortUsingSelector:@selector(compare:)];
}
[self saveBandsDictionary];
}
- (void)saveBandsDictionary
{
NSData *bandsDictionaryData = [NSKeyedArchiver
archivedDataWithRootObject:self.bandsDictionary];
[[NSUserDefaults standardUserDefaults] setObject:bandsDictionaryData
forKey:bandsDictionarytKey];
}
- (void)loadBandsDictionary
{
NSData *bandsDictionaryData = [[NSUserDefaults standardUserDefaults]
objectForKey:bandsDictionarytKey];
if(bandsDictionaryData)
{
self.bandsDictionary = [NSKeyedUnarchiver
unarchiveObjectWithData:bandsDictionaryData];
self.firstLettersArray = [NSMutableArray
arrayWithArray:self.bandsDictionary.allKeys];
[self.firstLettersArray sortUsingSelector:@selector(compare:)];
}
else
{
self.bandsDictionary = [NSMutableDictionary dictionary];
self.firstLettersArray = [NSMutableArray array];
}
} - Modify the viewDidLoad method with the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
[self loadBandsDictionary];
}
How It Works
The first code you implement overrides the compare: method for the WBABand class to compare two instances using the name property. Next, you declare the NSMutableDictionary to hold all the bands and the NSMutableArray for the first letters of the band names. You also declare the methods for adding, saving, and loading the bands from NSUserDefaults.
In the implementation of the addNewBand: method, you get the first letter of the band name using the substring method. Next, you look in the dictionary to see if you already have a band with that first letter. If so, you would find an NSMutableArray for the letter. If not, you create a new one. You then add the band to the array and sort it. You then look for the first letter in the firstLettersArray. If it is not there, you add it in and then sort that array as well.
Finally, you add code to save and retrieve the dictionary to NSUserDefaults. The code to do this is the same as when you save a single WBABand instance, because the WBABand class implements the NSCoding protocol, as does the NSMutableDictionary.
Adding Bands
In the last chapter you built the user interface for adding a band. The scene is no longer visible, though, since adding the Navigation Controller to the project. Instead you can present it from the WBABandsListTableViewController. The user will tap a button you will place on the UINavigationItem when they want to add a new band.
First you need to make some modifications to the ViewController class, starting with renaming it following the Apple naming conventions. Xcode makes this easy using its Refactor feature.
TRY IT OUT: Renaming a Class Using Xcode Refactoring
- Select the ViewController.h class from the Project Navigator.
- Right click on the ViewController class name to bring up the context menu as shown in Figure 5-10 and select Refactor ⇒ Rename...
FIGURE 5-10
- Rename the class WBABandDetailsViewController and click Preview.
- Review the changes then click Save.
- Xcode will prompt you to enable snapshots, which are a lightweight type of version control. Whether or not you want to use them is up to you.
- Compile the project and verify there are no errors.
How It Works
The refactor feature in Xcode is very handy. Renaming a class can be tricky, since you need to find every place in the project where the name is used and change it. The Refactor feature finds all of them for you. You can see how many places are being changed in the preview before you click Save.
Snapshots in Xcode give you a way to rollback the entire project should the refactoring go wrong. The decision to use them is up to you. If you are not using any other type of source control like Git or Mercurial, then you should use them.
NOTE The AppDelegate class should also be renamed to WBAAppDelegate. You won’t be modifying any code in this class, but the sample code does have it renamed.
Changing the name of the class to WBABandDetailsViewController also changes how its scene appears in the Storyboard. With multiple scenes in a Storyboard, Interface Builder uses the dock of each to show a friendly name, as shown in Figure 5-11. This book will use these friendly names to refer to different scenes in the Storyboard for readability. For example the Band Details View Controller will be referred to as the Band Details scene.
FIGURE 5-11
The next step is to clean up the code and add a Save button to the Band Details scene. There is no longer a need to store just a single WBABand instance in NSUserDefaults, so that code can be removed. You also need a way to tell the WBATableViewController that users want to save the new band they just added.
TRY IT OUT: Cleaning Up the WBABandDetailsViewController
- Select the Main.storyboard from the Project Navigator.
- In the Band Details View Controller move the Delete UIButton to be aligned with the right guideline of the UIView.
- Drag a new UIButton onto the view, set its text to Save, and align it with the Delete UIButton and the left guideline of the view, as shown in Figure 5-12.
FIGURE 5-12
- Add an auto layout constraint to anchor the Save UIButton to the bottom of the UIView.
- Select the WBABandDetailsViewController.h file from the Project Navigator, and add the following code:
@interface WBABandDetailsViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate, UIActionSheetDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@property (nonatomic, weak) IBOutlet UIButton *saveNotesButton;
@property (nonatomic, weak) IBOutlet UIStepper *ratingStepper;
@property (nonatomic, weak) IBOutlet UILabel *ratingValueLabel;
@property (nonatomic, weak) IBOutlet UISegmentedControl
*touringStatusSegmentedControl;
@property (nonatomic, weak) IBOutlet UISwitch *haveSeenLiveSwitch;
@property (nonatomic, assign) BOOL saveBand;
- (IBAction)saveNotesButtonTouched:(id)sender;
- (IBAction)ratingStepperValueChanged:(id)sender;
- (IBAction)tourStatusSegmentedControlValueChanged:(id)sender;
- (IBAction)haveSeenLiveSwitchValueChanged:(id)sender;
- (IBAction)deleteButtonTouched:(id)sender;
- (IBAction)saveButtonTouched:(id)sender;
- (void)saveBandObject;
- (void)loadBandObject;
- (void)setUserInterfaceValues;
@end - Remove the following lines from the interface:
- (void)saveBandObject;
- (void)loadBandObject; - Select the WBABandDetailsViewController.m file from the Project Navigator, and add the following code to the implementation:
- (IBAction)saveButtonTouched:(id)sender
{
if(self.bandObject.name && self.bandObject.name.length > 0)
{
self.saveBand = YES;
[self dismissViewControllerAnimated:YES completion:nil];
}
else
{
UIAlertView *noBandNameAlertView = [[UIAlertView alloc]
initWithTitle:@"Error" message:@"Please supply a name for the band"
delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[noBandNameAlertView show];
}
} - Modify the actionSheet:clickedButtonAtIndex: method with the following code:
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(actionSheet.destructiveButtonIndex == buttonIndex)
{
self.bandObject = nil;
self.saveBand = NO;
[self dismissViewControllerAnimated:YES completion:nil];
}
} - Remove the saveBandObject and loadBandObject methods along with all calls to those methods throughout the implementation.
- Select the Main.storyboard from the Project Navigator.
- Connect the Save UIButton to the saveButtonTouched: method you added to the WBABandDetailsViewController.
How It Works
In the Storyboard you added a Save UIButton to the Band Details scene. In the WBABandDetailsViewController class you declared a new boolean named saveBand as well as a new IBAction method named saveButtonTouched:,which you then connected to the Save UIButton. When users tap Save, this method validates that the bandObject has a name then sets the saveBand flag to TRUE before calling dismissViewControllerAnimated:completion:, which does nothing at this point but will dismiss the view after you complete the next Try It Out. If the bandObject does not have a name, the code uses a UIAlertView to tell users that a name is required.
The code is now ready. The last step is presenting the Band Details scene from the Bands List. In iOS apps it is customary to use a UIBarButtonItem with the + icon to add new data. You use UIBarButtonItem when adding buttons to a UINavigationItem. The Bands app will use this approach in the following Try It Out.
TRY IT OUT: Adding Band Objects
- Select the Main.storyboard from the Project Navigator.
- Drag a new Bar Button Item from the Object library to the left side of the UINavigationItem.
- In the Attributes Inspector, change the button style to Add, which changes the icon to a +, as shown in Figure 5-13.
FIGURE 5-13
- Select the Band Details scene.
- In the Identity Inspector, set the Storyboard ID to bandDetails.
- Select the WBABandsListTableViewController.h file from the Project Navigator, and add the following code:
@class WBABand, WBABandDetailsViewController;
@interface WBABandsListTableViewController : UITableViewController
@property (nonatomic, strong) NSMutableDictionary *bandsDictionary;
@property (nonatomic, strong) NSMutableArray *firstLettersArray;
@property (nonatomic, strong) WBABandDetailsViewController
*bandDetailsViewController;
- (void)addNewBand:(WBABand *)bandObject;
- (void)saveBandsDictionary;
- (void)loadBandsDictionary;
- (IBAction)addBandTouched:(id)sender;
@end - Select the WBABandsListTableViewController.m file from the Project Navigator.
- Add the WBABandDetailsViewController.h file to the imports with the following code:
#import "WBABandsListTableViewController.h"
#import "WBABand.h"
#import "WBABandDetailsViewController.h" - Add the following code to the implementation:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(self.bandDetailsViewController && self.bandDetailsViewController.saveBand)
{
[self addNewBand:self.bandDetailsViewController.bandObject];
self.bandDetailsViewController = nil;
}
}
- (IBAction)addBandTouched:(id)sender
{
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@"Main"
bundle:nil];
self.bandDetailsViewController = (WBABandDetailsViewController *)
[mainStoryboard instantiateViewControllerWithIdentifier:@"bandDetails"];
[self presentViewController:self.bandDetailsViewController animated:YES
completion:nil];
} - Select the Main.storyboard from the Project Navigator.
- Connect the Add button to the addBandTouched: method you added to the WBABandsListTableViewController.
- Run the app in the iPhone 4-inch simulator. When you tap the Add button, the Band Details scene displays.
How It Works
The first thing you do is add a button to the UINavigationItem. A UINavigationItem has both a left and a right button. Typically, the left button is used for navigating back up the navigation stack, but because this is the root UIViewController, you can use the left button for adding bands.
The main lesson is how to show and dismiss a modal view. You first set the Storyboard identity of the Band Details scene. In the addBandTouched: method you use an instance of the Storyboard and the scenes Storyboard ID to initialize a copy of the WBABandDetailsViewController. You set it as a property of the WBABandsListTableViewController to get to the bandObject after the view is dismissed. Finally, you use presentViewController:animated: and dismissViewControllerAnimated: to show and hide the band info view.
Displaying Bands
Now that you have implemented the Bands data source along with a way to add new bands, it’s time to display the bands in the UITableView. Most of the hard work is done. You just need to modify the UITableViewDataSource methods to use the Bands dictionary and reload the UITableView data when a new band is added.
TRY IT OUT: Displaying Band Names
- Select the WBABandsListTableViewController.m file from the Project Navigator.
- Modify the numberOfSectionsInTableView: method with the following code:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return self.bandsDictionary.count;
} - Modify the tableView:numberOfRowsInSection: method with the following code:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
NSString *firstLetter = [self.firstLettersArray objectAtIndex:section];
NSMutableArray *bandsForLetter = [self.bandsDictionary
objectForKey:firstLetter];
return bandsForLetter.count;
} - Modify the tableView:cellForRowAtIndexPath: method with the following code:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSString *firstLetter = [self.firstLettersArray
objectAtIndex:indexPath.section];
NSMutableArray *bandsForLetter = [self.bandsDictionary
objectForKey:firstLetter];
WBABand *bandObject = [bandsForLetter objectAtIndex:indexPath.row];
// Configure the cell...
cell.textLabel.text = bandObject.name;
return cell;
} - Modify the viewWillAppear: method with the following code:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(self.bandDetailsViewController)
{
if(self.bandDetailsViewController.saveBand)
{
[self addNewBand:self.bandDetailsViewController.bandObject];
[self.tableView reloadData];
}
self.bandDetailsViewController = nil;
}
} - Run the app in the iPhone 4-inch simulator. When you add a new band, it displays in the table, as shown in Figure 5-14.
FIGURE 5-14
How It Works
The UITableViewDataSource methods use an NSIndexPath to refer to rows in the table. An NSIndexPath has a section and row number. NSMutableDictionary is a key/value collection, not a sorted set, so it doesn’t have an objectAtIndex: method you can use with an NSIndexPath. Because of this you need to use the firstLetterArray to get the correct key to use with the Bands dictionary. It may seem like more work than it’s worth, but the firstLetterArray is also used to implement section headers and the index, so it’s worth the effort.
In the code you modify the numberOfSectionsInTableView: to return the number of items in the Bands dictionary. In the tableView:numberOfRowsInSection: method, you use the section number to get the correct key from the firstLettersArray, then use that key to get the bands array for that key, and use its count for the number of rows. The tableView:cellForRowAtIndexPath: is modified to also use the section to get the correct key from the firstLettersArray. You then use the row of the index path to get the correct WBABand instance to configure the cell with.
Finally, you add a viewWillAppear: method to the WBATableViewController. This method is part of the UIViewControllerDelegate and gets called when the view is about to appear. In the Bands app it is called when the app first starts but also after the WBABandDetailsViewController is dismissed. To know which occurred, you look at the bandDetailsViewController property you set prior to presenting the WBABandDetailsViewController. If it’s set, you check if the user tapped Save by looking at the saveBand property. If true, you save the new bandObject and reload the tableView. Finally, you set the bandDetailsViewController to nil, so the save code is not triggered twice by accident.
IMPLEMENTING SECTIONS AND INDEX
As users of the Bands app add more and more bands, it becomes harder to find the band they want in the UITableView. To help them out you’ve already alphabetically ordered the bands. Adding sections and the section index can help as well. The sections break up the bands visually while the index gives the user a shortcut to jump to the different sections.
Adding Section Headers
Section headers are simple to add because of the data storage architecture you implemented using both the dictionary and the first letters array. There is a single method in the UITableViewDataSource protocol, which returns the section header for the section.
TRY IT OUT: Displaying First Letter Section Headers
- Select the WBABandsListTableViewController.m file from the Project Navigator.
- Add the following code to the implementation:
- (NSString *)tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section
{
return [self.firstLettersArray objectAtIndex:section];
} - Run the app in the iPhone 4-inch simulator. You see section headers corresponding to the first letters of the band names, as shown in Figure 5-15.
FIGURE 5-15
How It Works
As the UITableView is loaded and scrolled, it calls its data source and looks for header titles for each section using the tableView:titleForHeaderInSection: method. Your code simply returns the letter in the firstLettersArray at the index matching the section number.
Showing the Section Index
The section index is slightly more complicated because you need to supply both the titles for the index and the section that corresponds with the title. You need to implement two more methods of the UITableViewDataSource to accomplish this.
TRY IT OUT: Displaying First Letter Section Index
- Select the WBABandsListTableViewController.m file from the Project Navigator.
- Add the following code to the implementation:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
return self.firstLettersArray;
}
- (int)tableView:(UITableView *)tableView sectionForSectionIndexTitle:
(NSString *)title atIndex:(NSInteger)index
{
return [self.firstLettersArray indexOfObject:title];
} - Run the app in the iPhone 4-inch simulator. You see the section index on the right side of the UITableView, as shown in Figure 5-16.
FIGURE 5-16
How It Works
The first method you implement is sectionIndexTitlesForTableView. This method tells the UITableView what strings to put in the index. The tableView:sectionForSectionIndexTitle: method tells the UITableView what section to jump to based on the title in the index that was touched.
EDITING TABLE DATA
UITableView also give you a way to edit the underlying data source. You can control which rows are editable along with the type of editing that can be done. The most used is deleting data, but you could also give your user a way to reorder the data in the data model. Because the Band app displays the band names alphabetically, it doesn’t make sense to allow the user to move cells around. Deleting a band does make sense, though. To do this you need to enable edit mode for both the table and each individual row.
Enabling Edit Mode
UITableView has an edit mode built in. UITableViewController also has a built-in button you can add to the UINavigationItem to toggle into edit mode named editButtonItem. Since the WBABandsListTableViewController is a subclass of the UITableViewController, you can access the editButtonItem from self.
When a table goes into edit mode, it asks its delegate which rows are editable. You can control which rows are editable by implementing the tableView:canEditRowAtIndexPath: method. If this method is not implemented, the table will not allow any of the rows to be editable.
TRY IT OUT: Implementing the Allow Edit Method
- Select the WBABandsListTableViewController.m file from the Project Navigator.
- Add the following code to the viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
[self loadBandsDictionary];
self.navigationItem.rightBarButtonItem = self.editButtonItem;
} - Add the following method to the implementation:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:
(NSIndexPath *)indexPath
{
return YES;
} - Run the app in the iPhone 4-inch simulator. When you tap the Edit button, you see the delete option in each cell next to the band name, as shown in Figure 5-17.
FIGURE 5-17
How It Works
When the view is loaded, the code you add sets the right button of the UINavigationItem to the editButtonItem built into the UITableViewController. The tableView:canEditRowAtIndexPath: always returns YES, meaning that all rows in the UITableView are editable.
Deleting Cells and Data
You probably noticed that when the UITableView goes into edit mode and you attempt to delete a row, nothing happens. This is because you need to implement one last method of the UITableViewDelegate, the tableView:commitEditingStyle:forRowAtIndexPath: method.
TRY IT OUT: Implementing the Commit Edit Method
- Select the WBABandsListTableViewController.h file from the Project Navigator, and add the following code to the interface:
@class WBABand, WBABandDetailsViewController;
@interface WBABandsListTableViewController : UITableViewController
@property (nonatomic, strong) NSMutableDictionary *bandsDictionary;
@property (nonatomic, strong) NSMutableArray *firstLettersArray;
@property (nonatomic, strong) WBABandDetailsViewController
*bandDetailsViewController;
- (void)addNewBand:(WBABand *)bandObject;
- (void)saveBandsDictionary;
- (void)loadBandsDictionary;
- (void)deleteBandAtIndexPath:(NSIndexPath *)indexPath;
- (IBAction)addBandTouched:(id)sender;
@end - Select the WBABandsListTableViewController.m file from the Project Navigator, and add the following methods to the implementation:
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[self deleteBandAtIndexPath:indexPath];
}
}
- (void)deleteBandAtIndexPath:(NSIndexPath *)indexPath
{
NSString *sectionHeader = [self.firstLettersArray
objectAtIndex:indexPath.section];
NSMutableArray *bandsForLetter = [self.bandsDictionary
objectForKey:sectionHeader];
[bandsForLetter removeObjectAtIndex:indexPath.row];
if(bandsForLetter.count == 0)
{
[self.firstLettersArray removeObject:sectionHeader];
[self.bandsDictionary removeObjectForKey:sectionHeader];
[self.tableView deleteSections:
[NSIndexSet indexSetWithIndex:indexPath.section]
withRowAnimation:UITableViewRowAnimationFade];
}
else
{
[self.bandsDictionary setObject:bandsForLetter forKey:sectionHeader];
[self.tableView deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
}
[self saveBandsDictionary];
}
How It Works
In the implementation you first look to see if the editing style is a delete. If it is you call the deleteBandAtIndexPath: method. It uses the indexPath to again get the key to the bandsForLetter array. It then removes the WBABand instance by calling the removeObjectAtIndex: method using the row of the indextPath. Next, you check to see if there are any bands left for the first letter. If there are no more bands, you delete the entire section from the table. If there are, you delete only the row in the section.
WARNING It is very important to delete the section and not just the row if there are no more bands for that letter. Failing to do so leaves a section in the UITableView with no rows. This will cause the app to crash. Also be sure to make all the changes to the data source prior to deleting the section or the row. The app will crash in that situation as well.
Modifying Data
Users of the Bands app will want to modify band info along with adding and deleting it. The best way to do this is to show the Band Details scene when the user taps the band name in the UITableView. You can implement a segue in the Storyboard to do this.
A segue is a way of transitioning from one view to the next. Modal and push segues are the two most common. A modal segue presents the view over the parent view, the same as using presentViewController:animated:completion:, as you did with the Add button. You could have used a segue for this, but learning how to present and dismiss views in code is a valuable lesson.
A push segue can be used with a UINavigationController. The view slides in from the right and adds a UINavigationItem to the top of the view with a back button, which enables the user to return to the parent UIViewController. It does this by using a navigation stack. As you segue from one UIViewController to the next, the UIViewControllers get pushed onto the navigation stack. The back button pops each UIViewController off until you get back to the root UIViewController. This makes it easy for users to know where they are in the app. Storyboards make it easy for you as the developer to visualize how the user can navigate through the app.
TRY IT OUT: Implementing a Push Segue
- Select the Main.storyboard from the Project Navigator.
- In the Band Details scene, move all the subviews down 20 pixels except for the Save and Delete buttons.
- Control-drag from the prototype cell to the Band Details scene, and select Push from the segue pop-up menu that appears. The segue is represented by an arrow from the Bands List scene to the Band Details scene. The Band Details scene also now has a UINavigationItem.
- Select the UINavigationItem and set its title to Band in the Attributes Inspector.
- Select the WBABandsListTableViewController.h file from the Project Navigator, and add the following code to the interface:
@class WBABand, WBABandDetailsViewController;
@interface WBABandsListTableViewController : UITableViewController
@property (nonatomic, strong) NSMutableDictionary *bandsDictionary;
@property (nonatomic, strong) NSMutableArray *firstLettersArray;
@property (nonatomic, strong) WBABandDetailsViewController
*bandDetailsViewController;
- (void)addNewBand:(WBABand *)bandObject;
- (void)saveBandsDictionary;
- (void)loadBandsDictionary;
- (void)deleteBandAtIndexPath:(NSIndexPath *)indexPath;
- (void)updateBandObject:(WBABand *)bandObject
atIndexPath:(NSIndexPath *)indexPath;
- (IBAction)addBandTouched:(id)sender;
@end - Select the WBABandsListTableViewController.m file from the Project Navigator.
- Add the following code to the viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
[self loadBandsDictionary];
self.navigationItem.rightBarButtonItem = self.editButtonItem;
self.clearsSelectionOnViewWillAppear = NO;
} - Modify the viewWillAppear: method with the following code:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(self.bandDetailsViewController)
{
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
if(self.bandDetailsViewController.saveBand)
{
if(selectedIndexPath)
{
[self updateBandObject:self.bandDetailsViewController.bandObject
atIndexPath:selectedIndexPath];
[self.tableView deselectRowAtIndexPath:selectedIndexPath
animated:YES];
}
else
[self addNewBand:self.bandDetailsViewController.bandObject];
[self.tableView reloadData];
}
else if (selectedIndexPath)
{
[self deleteBandAtIndexPath:selectedIndexPath];
}
self.bandDetailsViewController = nil;
}
} - Add the following methods to the implementation:
- (void)updateBandObject:(WBABand *)bandObject
atIndexPath:(NSIndexPath *)indexPath
{
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
NSString *sectionHeader = [self.firstLettersArray
objectAtIndex:selectedIndexPath.section];
NSMutableArray *bandsForSection = [self.bandsDictionary
objectForKey:sectionHeader];
[bandsForSection removeObjectAtIndex:indexPath.row];
[bandsForSection addObject:bandObject];
[bandsForSection sortUsingSelector:@selector(compare:)];
[self.bandsDictionary setObject:bandsForSection forKey:sectionHeader];
[self saveBandsDictionary];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
NSString *sectionHeader = [self.firstLettersArray
objectAtIndex:selectedIndexPath.section];
NSMutableArray *bandsForSection = [self.bandsDictionary
objectForKey:sectionHeader];
WBABand *bandObject = [bandsForSection objectAtIndex:selectedIndexPath.row];
self.bandDetailsViewController = segue.destinationViewController;
self.bandDetailsViewController.bandObject = bandObject;
self.bandDetailsViewController.saveBand = YES;
} - Select the WBABandDetailsViewController.m file from the Project Navigator.
- Modify the saveButtonTouched: method with the following code:
- (IBAction)saveButtonTouched:(id)sender
{
if(!self.bandObject.name || self.bandObject.name.length == 0)
{
UIAlertView *noBandNameAlertView = [[UIAlertView alloc]
initWithTitle:@"Error" message:@"Please supply a name for the band"
delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[noBandNameAlertView show];
}
else
{
self.saveBand = YES;
if(self.navigationController)
[self.navigationController popViewControllerAnimated:YES];
else
[self dismissViewControllerAnimated:YES completion:nil];
}
} - Modify the actionSheet:clickedButtonAtIndex: method with the following code:
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(actionSheet.destructiveButtonIndex == buttonIndex)
{
self.bandObject = nil;
self.saveBand = NO;
if(self.navigationController)
[self.navigationController popViewControllerAnimated:YES];
else
[self dismissViewControllerAnimated:YES completion:nil];
}
} - Run the app in the iPhone 4-inch simulator. You can now see the accessoryView for each row is set to a chevron. When a row is selected, you can segue to the Band Details scene with all the UI objects set according to the bandObject.
How It Works
By Control-dragging from the prototype cell to the Band Details scene, you create a segue. When the app runs, tapping a cell starts the segue, which first calls the prepareForSegue:sender: method. In this method you get the NSIndexPath of the selected row in the tableView and set the bandObject of the WBABandDetailsViewController with the bandObject in the data source.
In the WBABandDetailsViewController, you change how it is dismissed based on if it has a UINavigationController. If it does, you call popViewControllerAnimated: to return back to the WBABandsListViewController. When it appears, it looks to see if it has a row selected. If it does, it knows to either update a band if saveBand is true or delete the band if not.
SUMMARY
UITableViews are powerful. They can show a lot of data in a scrollable view and enable the user to edit that data. Apple has given you many tools to do these actions. You learned how to add a UITableView to a Storyboard and set up its UITableViewController identity class, UITableViewDataSource, and UITableViewDelegate. You modified the Bands app to give it the ability to store many WBABand instances in the data model and display them in the new Bands List scene. You also learned how to use the UINavigationItem of a UINavigationController to add data to the Bands app, enable edit mode to delete the data, and to segue from the Bands List scene to the Band Details scene. With the UITableView as the main scene of the app that segues to a details scene, you have transitioned the Bands app from a Single View Application to a Master-Details application.
EXERCISES
- What is the difference between the UITableViewDataSource protocol and the UITableViewDelegate protocol?
- What are the four built-in UITableViewCell styles?
- Modify the UITableViewCell to a right detail style, and show the band rating as the detailTextLabel.
- What method can you use to show a UIViewController that animates up from the bottom of the bottom of the screen?
- What is the UIKit object that is added to the top of each UIView when using a UINavigationController?
- What type of segue is used to transition between the Bands List scene and the Band Details scene?
WHAT YOU LEARNED IN THIS CHAPTER
TOPIC KEY CONCEPTS
UITableView The UITableView is one of the most important views in iOS development. It’s a scrollable list of cells that can show all of the objects in your data model. The Bands app uses one to show all of the bands the user has added to the app.
UITableViewCell Each row in a UITableView is represented by a UITableViewCell. They can have a custom design you create, or you can use one of the standard cell styles provided by Apple.
UITableViewDataSource The UITableView gets the data from the data model via its controller using the UITableViewDataSource protocol. You use this to tell the table how many sections it has and how many rows are in each section, as well as create the actual UITableViewCells to display.
UITableViewDelegate Users can edit the data model of an application from within a UITableView. When in edit mode users can move or delete rows. When this happens the table will communicate with its controller using the UITableViewDelegate protocol.
UINavigationController The UINavigationController allows your application to push or pop new views onto the navigation stack using segues. When you use it with a UITableView, you can transition from the list of data model objects to the details of a single object.