Finishing the Interface
To complete the interface for the MatchPicker application, we need two labels (UILabel)—one for providing feedback of what the user just selected, another for showing whether the user successfully made a match. These labels will, in turn, connect to the lastAction and matchResult outlets, respectively.
Adding the Output Labels
Drag two labels into the view, positioning one above the other. Change the title of the top label to read Last Action and the bottom label to read Match Feedback.
In our sample project, we’ve also chosen to set the attributes so that
both labels are centered and the Match Feedback label is larger (24 pt)
than the Last Action label, as shown in Figure 4.
Connecting the Outlets
Connect each of the labels to
their corresponding outlets by Control-dragging from the File’s Owner
icon to each label and then choosing the lastAction and matchResult outlets, as appropriate. Figure 5 demonstrates the connection from the Match Feedback label to its matchResult outlet.
With that simple step, we’re
done with Interface Builder. All the work of actually customizing the
appearance of the picker view must take place in Xcode.
Providing Data to the Picker
A big difference between the UIPickerView we’re using now and other controls, such as the UISegmentedControl,
is that what the control displays is determined entirely by the code we
write. There is no “point and click” to edit the different components
and values within Interface Builder. WYSIWYG isn’t an option.
So,
what information do we need to provide? Remember that the picker
displays scrolling wheels called components. Within each component are
any number of “rows” that display the values you want users to select.
We’ll need to provide the picker with data for each row in each
component. For this example, we’ll have one component with animal names
and another with animal sounds.
Creating the Application Data Structures
Because the picker
displays lists of information, it stands to reason that we’d want to
store the data that they display as lists—a perfect job for an array!
We’ll create two arrays, animalNames and animalSounds, that contain all the information that the picker will display to the user.
We want these arrays to be available to everything in the MatchPickerViewController class, so we first need to add them to the @interface block within the view controller header file. Edit MatchPickerViewController.h to include two NSArrays (animalNames and animalSounds) as shown in Listing 2.
Listing 2.
#import <UIKit/UIKit.h>
@interface MatchPickerViewController : UIViewController <UIPickerViewDataSource, UIPickerViewDelegate> { NSArray *animalNames; NSArray *animalSounds; IBOutlet UILabel *lastAction; IBOutlet UILabel *matchResult; }
@property (nonatomic, retain) UILabel *lastAction; @property (nonatomic, retain) UILabel *matchResult;
@end
|
These
two arrays correspond to the components that we’ll be displaying in the
picker. Components are numbered starting at zero, left to right, so,
assuming we want the names of the animals to be on the left and sounds
on the right, component 0 will correspond to the animalNames array and component 1 to animalSounds.
Before going any further, take a few seconds to add the appropriate releases for these arrays within the dealloc method. The current version of the method should read as follows:
- (void)dealloc {
[animalNames release];
[animalSounds release];
[lastAction release];
[matchResult release];
[super dealloc];
}
Populating the Data Structures
After the arrays have been declared, they need to be filled with data. The easiest place to do this is in the viewDidLoad method of the view controller (MatchPickerViewController.m). Edit MatchPickerViewController.m by uncommenting the viewDidLoad method and adding the lines, as shown in Listing 3, to allocate and initialize the arrays with a list of animals and sounds.
Listing 3.
- (void)viewDidLoad { animalNames=[[NSArray alloc]initWithObjects: @"Mouse",@"Goose",@"Cat",@"Dog",@"Snake",@"Bear",@"Pig",nil]; animalSounds=[[NSArray alloc]initWithObjects: @"Oink",@"Rawr",@"Ssss",@"Roof",@"Meow",@"Honk",@"Squeak",nil]; }
|
Watch Out!
nil is needed to denote the end of the array initialization list, so if it’s missing, your application will almost certainly crash!
The arrays are initialized with a
series of strings, but if you look closely, the strings don’t match up!
This is intentional. It wouldn’t make sense to display the strings so
that the animal name immediately matches the animal sound. Instead,
element 0 of animalNames matches element 6 of animalSounds, animalNames element 1 matches animalSounds
element 5, 2 matches 4, and so on. When it gets time to handle checking
for a match between the components, we’ll use this logic:
The total number of sounds, minus 1, minus the sound the user has chosen must be the same as the chosen animal.
So, for a total of 7
sounds, where element 5 is chosen, we get 7 – 1 – 5 = 1 (exactly the
number we want). You’re welcome to implement your own display and
matching logic. This is just a quick way of “mixing things up” a bit for
the purposes of this project.
To help simplify the
application a bit, it would be nice if we could symbolically refer to
“component 0” as the “animalComponent” and “component 1” as the
“soundComponent.” By defining a few constants at the start of our
implementation file, we can do just that! Edit
MatchPickerViewController.m and add these lines so that they precede the
#import line:
#define componentCount 2
#define animalComponent 0
#define soundComponent 1
The first constant componentCount is just the number of components that we want to display in the picker, whereas the other two constants, animalComponent and soundComponent, can be used to refer to the different components in the picker without resorting to using their actual numbers.
What’s Wrong with Referring to Something by its Number?
Absolutely nothing. The reason
that it is helpful to use constants, however, is that if your design
changes and you decide to change the order of the components or add
another component, you can just change the numbering within the
constants rather than each place they’re used in the code. This will
make a bit more sense in a few minutes as we start implementing the
delegate and data source protocol methods.
Our application has everything it requires data-wise—it just needs the methods to get the data into the picker view.
Implementing the Picker Data Source Methods
Despite a promising sounding name, the picker data source methods (described by the UIPickerViewDataSource protocol) really only provide a small amount of information to the picker via these methods:
numberOfComponentsInPickerView: Returns the number of components the picker should display
pickerView:numberOfRowsInComponent: Returns the number of rows that the picker will be displaying within a given component
Let’s start with component count. Edit MatchPickerViewController.m and add the following method to the file:
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return componentCount;
}
We already defined a constant, componentCount, with the number of components we want to display (2), so the entire implementation of this method is just the line return componentCount.
The second method, pickerView:numberOfRowsInComponent, is expected to return the number of rows contained within a given component. We’ll make use of the NSArray method count
to return the number of items within the array that is going to make up
the component. For example, to get back the number of names in the animalNames array, we can use this:
Implement it using the code in Listing 4.
Listing 4.
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { if (component==animalComponent) { return [animalNames count]; } else { return [animalSounds count]; } }
|
Here, we just compare the component variable provided when the picker calls the method to the animalComponent constant we declared earlier. If they are equal, we return a count of the names in animalNames. Otherwise, we return a count of the number of sounds in animalSounds.
Congratulations, you’ve just completed the methods required for conforming to the UIPickerViewDataSource protocol!
Populating the Picker Display
Where the data source protocol methods are responsible for defining how many items will appear in the picker, the UIPickerViewDelegate methods define what items are shown, and how they are displayed. There’s only a single method we really need before we can start looking at the results of our work: pickerView:titleForRow:forComponent. This method is called by the picker view to determine what text should be shown in a given component and row.
For example, if component 0 and row 0 are provided to the method as parameters, the method should return Mouse, because it is the first element of our animalNames array, which corresponds to component 0 in the picker.
This method requires the ability to retrieve a string from one of our arrays. The NSArray instance method objectAtIndex is exactly what we need. To retrieve row 5 from the animalSounds array we could use this:
[animalSounds objectAtIndex:5]
The pickerView:titleForRow:forComponent method provides us with both a row variable and a component variable. The implementation is shown in Listing 5.
Listing 5.
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { if (component==animalComponent) { return [animalNames objectAtIndex:row]; } else { return [animalSounds objectAtIndex:row]; } }
|
The code first checks to see whether the supplied component variable is equal to the animalComponent constant that we configured, and then, if it is, returns the object (a string) from the specified row of the animalNames array. If component isn’t equal to animalComponent, we can assume that we need to be looking at the animalSounds array and return a string from it instead.
By the Way
Because we have just two
components, we’re making the assumption that if a method isn’t
referencing one, it must be referencing the other. Obviously if you have
more than two components, you need a more complicated if-then-else
structure or a switch statement.
After
adding the method to MatchPickerViewController.m, save the file, and
then choose Build and Run. The application will launch and show the
picker view, complete with the contents of your two arrays, much like Figure 6.
Notice that although the
picker does work, choosing values has no effect—that’s because we need
to implement one more delegate method before our efforts will truly pay
off.