第七章 标签栏与选取器
本章将构建一个完整的标签栏应用程序,它包含5个不同的标签和5个不同的内容视图。构建此应用程序能巩固上一章介绍的知识,
并展示如何使用控件--选取器视图(picker view),人们经常称之为选取器。
选取器是带有能够旋转的刻度盘的控件,他们用于在Calendar应用程序中输入日期,或者在Clock应用程序中设置计时器。
选取器可以配置为显示一个或多个刻度盘。默认情况下,选取器显示文本列表,但是他们也能够显示图像。
7.1 Pickers应用程序
本章的Pickers应用程序包含一个标签栏。在构建该应用程序时,我们将更改默认的标签栏,使其包含5个标签,并向每个标签栏项
添加一个图标,然后创建一系列内容视图,并将每个视图连接到每个标签。
我们将构建的第一个内容视图包含一个日期选取器(data picker),这是最容易实现的选取器类型。该视图还有一个按钮,点击按
钮时,弹出警告,显示选取的日期。
第二个标签中的选取器包含一组值。此选取器的实现比日期选取器稍微难一些。
第三个标签中,我们将创建带有两个独立滚轮的选取器。从技术上说,滚轮应该称为选取器组件,也就是说,我们将在其中创建带
有两个组件的选取器。我们将了解如何使用数据源和委托选取器提供两个独立的数据列表。
第4个标签中,我们就创建另一个带有两个组件的选取器。但是这一次,右侧组件中显示的值将根据左侧组件中选定值的变化而变化
。
第5个标签中,我们将了解如何将图像数据添加到选取器,并编写一个小游戏来进行演示,该游戏使用一个带有5个组件的选取器。
做了一个“小型老虎机”的游戏。对于此选取器,用户无法手动更改组件的值,但是能够选择Spin按钮来使5个滚轮旋转到一个新的
、随机选定的值。如果在一行中出现了3个完全一样的图片,则表明用户获胜!
7.2 委托和数据源
让我们看一下为什么选取器更复杂。更复杂不仅仅意味着IB中有更多可配置属性需要设置。实际上,只有很少的选取器属性能够在
IB中配置。要使用选取器,并不是从IB中挑选一个选取器并放在内容视图上,然后进行配置就行了,但是日期选取器是个例外。除
此之外,还需要为选取器提供“选取器代理”(picker delegate)和选取器数据源(picker datasource)。
选取器委托最重要的任务是,确定要为每个组件中的每一行绘制的实际内容。选取器要求委托在特定组件上的特定位置绘制一个字
符串或一个视图。
除了委托之外,选取器还必须包含一个数据源。数据源的工作原理与委托类似,因为他的方法将在预先指定的时刻被调用。选取器
使用数据源方法获取组件数和每个组件中的行数。如果未指定数据源和委托,选取器就无法工作,甚至无法绘制选取器。
在很多情况下,数据源和委托是同一个对象,该对象也是包含选取器的视图的视图控制器,我们将在本章的应用程序中采用这种方
法。每个内容窗格的视图控制器都将是其选取器的数据源和委托。
说明:选取器数据源是应用程序的模型、视图和控制器的哪部分呢?这是一个难以回答的问题。数据源似乎应该是模型的一部分,
但是实际上,他是控制器的一部分。数据源并不总是一个用于保存数据的对象,他是为选取器提供数据的对象。
7.3 建立工具栏框架
Xcode没有为标签栏应用程序提供模版,因此我们将从头构建自己的模版。
创建一个新项目,选择Window-Based Application模版。输入Pickers做为模版名称。
7.3.1 创建文件
在上一章中,我们创建了一个根视图控制器。这次还将创建一个根视图控制器,但是无需为其创建一个i额类,因为苹果公司提供了
一个非常不错的类来管理标签栏视图,所以我们将使用UITabBarController实例来处理根控制器。
需要在Xcode中创建5个新类:根控制器进行切换的5个视图控制器。
单击Classes文件夹,按下Cmd+N或File=》New File...。
在新建文件窗口的左侧窗格中选择Cocoa Touch Classes,然后选择UIViewController subclass图标,单击Next,将第一个类命名
为DataPickerViewController.m,确保选中了Also create “DatePickerViewController.h”。
重复此步骤,将余下的类分别命名为:SingleComponentPickerViewController、DoubleComponentPickerViewController、
DependentComponentPickerViewController和CustomPickerViewController
接下来,单击Resources文件夹,再次按下Cmd+N或File=》New File...。这一次,在新建文件帮助窗口的左侧窗格中选择User
Interfaces,然后单击View XIB图标。我们需要5个nib,分别对应每个内容视图。将第一个nib命名为DataPickerView.xib,然后创
建其他4个nib文件:SingleComponentPickerView、DoubleComponentPickerView、DependentComponentPickerView和
CustomPickerView
7.3.2 设置内容视图nib
双击DatePickerView.xib以在IB中打开该文件。我们实际上并不打算构建自己的内容视图。我们将搭建标签视图应用程序的框架,
让基本的应用程序能够正常运行。必须确保每个nib文件都指向正确的File's Owner图标,文件所有者应该是对应的
UIViewController子类。对于DatePickerView.xib,控制器类必须是DatePickerViewController。单击File's Owner,然后Cmd+4打
开身份检查器,将类改为DatePickerViewController。
现在,按住Control键并将File's Owner图标拖到View图标,选择view输出口。最后,单击View图标,按Cmd+3打开大小检查器。将
视图的高度改为411像素。保存并关闭此nib。
对其余4个nib文件重复此步骤。
7.3.3 添加根视图控制器
我们将在IB中创建自己的根视图控制器,这将是一个UITabBarController实例。但是在创建根控制器之前,我们应该为其声明一个
输出口。单击PickersAppDelegate.h类,添加以下代码:
...
IBOutlet UITabBarController *rootController;
...
@property (nonatomic,retain) UITabBarController *rootController;
其实现文件添加如何等内容(添加的位置等不再详细指出):
@synthesize rootController;
[window addSubview:rootController.view];
[rootController release];
标签栏使用图标来表示每个标签,因此在转到IB之前,我们应该添加要使用的图标。图标大小应该是24*24px,并采用.png格式。图
标文件应该有一个透明的背景。一般而言,浅灰色图标最适合标签栏。不要为设置合适的标签栏外观发愁。iPhone为自动设置合适
的图标外观,就像设置应用程序的图标一样。
添加资源带项目:将图标文件所在位置拖到Resources文件夹。或者从Project菜单中选择Add to Project...
添加图标之后,双击MainWindow.xib,在IB中打开该文件。从库中拖一个Tab Bar Controller到nib的主窗口。需要确保将其拖到
MainWindow.xib的窗口而不是Window窗口,因为后者不支持拖放操作。
将标签栏控制器放置到nib的主窗口之后,将会出现一个新窗口。这个标签栏控制器就是我们的根视图控制器。
在nib的主窗口中单击TabBarController图标,按Cmd+1打开属性检查器。
我们关不的部分位于顶部---View Controllers部分。当所有视图控制器都设置完成之后,标签控制器的每个标签都将拥有一个视图
控制器。我们需要更改标签栏控制器,使其拥有5个标签,而不是两个。单击+号3次。
单击TabBarController窗口底部的标签栏。一定要单击最左边的标签。这应该选定与最左侧标签相对应的控制器,检查器应该更改
为对应的外观。即显示标签的属性。
我们在这里将每个标签的视图控制器与合适的nib相关联。最左侧的并且启动5个子视图中的第一个。保留Title字段为空,将NIB
name 指定为DatePickerView。不要包含.xib扩展名。
完成之后按下Cmd+4,打开与最左侧标签相关联的视图控制器的身份检查器。将类改为DatePickerViewController。
按下Cmd+1返回到属性检查器。单击标签栏中的第一个标签,片刻之后再次单击。这将使检查器再次更改。
通过在同一位置再次单击标签栏,对视图控制器的选择就改成了对标签栏项本身的选择。换句话说,第一次单击选择的是5个子视图
控制器中的第一个。第二次单击选择的是标签栏本身,可以设置他的标题和图标。
我们在这里设置标签栏项的图标和标题。将Title由Item 1改为Date;Image为clockicon.png图像。
现在对另外4个标签栏重复此过程。
接下来要在nib中执行的操作就是,按住Ctrl键并将PickresAppDelegate图标拖到TabBarController图标,选择rootController输出
口。保存nib文件,返回Xcode。
现在,标签栏和内容视图应该都衔接起来,并且能够正常工作了。编译并运行应用程序,启动之后的应用程序应该包含一个能够正
常工作的工具栏,单击一个标签应该会将其选中。现在,内容视图还没有任何内容,因此,更改将不会很大。但是如果所有组成部
分都运行良好,那么多视图应用程序的基本框架现在就已经建立并能够运行了。接下来可以开始设置每个内容视图了。
提示:如果在单击某个标签时,仿真器出了问题,不要惊慌!这很可能是因为漏掉了一个步骤,或者出现了输入错误。返回去检查
一下所有nib文件名;确保所有连接都正确;并确保所有类名都设置正确。
7.4 实现日期选取器
要实现日期选择器,需要一个输出口和一个操作。输出口用于从日期选取器提取值。操作用于显示警报。
单击DatePickerViewController.h,修改其内容:
IBOutlet UIDatePicker *datePicker;
@property (nonatomic,retain) UIDatePicker *datePicker;
- (IBAction)buttonPressed;
现在,打开DatePickerView.xib。从库中查找Date Picker,将其拖到View窗口。将其放置在视图顶部。不要为选取器使用蓝色引导
线,他应该会与视图边缘完美结合。
选中日期选取器,按下Cmd+1打开属性检查器。他有很多属性。我们将保留大部分值的默认设置,但是你可以看看每个选项的用途。
将选取器的范围限制为合理的日期。将Minimum日期改为1/1/1900,将Maximum改为12/31/2200。
然后拖出一个Button,放在日期选取器下面。设置标题为Select,并连接操作方法和输出口。
现在,实现DatePickerViewController.m:
@synthesize datePicker;
- (IBAction)buttonPressed {
NSDate *selected=[datePicker date];
NSString *message=[[NSString alloc] initWithFormat:@"The date and time you selected is :%@",selected];
UIAlertView *alert=[[UIAlertView alloc]
initWithTitle:@"Date and Time Selected"
message:message
delegate:nil
cancelButtonTitle:@"Yes ,I did."
otherButtonTitles:nil];
[alert show];
[alert release];
[message release];
}
- (void)viewDidLoad {
NSDate *now=[[NSDate alloc] init];
[datePicker setDate:now animated:YES];
[now release];
}
...
7.5 实现单个组件选取器
日期选取器非常简单,接下来看一下如何使用支持从一组值中进行选择的选取器。在本示例中,我们将创建一个NSArray来保存想要
在选取器中显示的值。选取器本身不会保存任何数据。他们调用其数据源和委托上的方法来获取需要显示的数据。选取器不会关心
底层数据位于何处。他在需要时才会请求数据,数据源和委托将通过相互协作来提供该数据。因此,数据可以来自一个静态列表,
也可以从一个文件或URL载入,甚至随时地组合或计算而来。
7.5.1 声明输出口和操作
通常,在IB中工作以前,我们需要确保输出口和操作已经位于控制器的头文件中。在Xcode中,单击
SingleComponentPickerViewController.h。此控制器类同时充当选取器的数据源和委托,因此,我们需要确保他符合这两个角色的
协议。此外,还需要声明一个输出口和一个操作:
@interface SingleComponentPickerViewController:UIViewController <UIPickerViewDelegate,UIPickerViewDataSource> {
IBOutlet UIPickerView *singlePicker;
NSArray *pickerData;
}
@property (nonatomic,retain) UIPickerView *singlePicker;
@Property (nonatomic,ratain) NSArray *pickerData;
- (IBAction)buttonPressed;
@end
7.5.2 构建视图
双击SingleComponentPickerView.xib,打开标签栏的第二个标签的内容视图。从库中选择一个Picker View,将其添加到nib的View
窗口,使其与视图顶部吻合,就像日期选取器视图一样。
放置好选取器之后,按住Ctrl键并将File's Owner拖动到选取器视图,然后选择singlePicker输出口。接下来,选中选取器,按
Cmd+2打开连接检查器。如果查看选取器视图可用的连接,将会看到前两项是DataSource和Delegate。将Datasource旁边的圆圈拖到
File Owner图标。然后再次将Delegate拖到Files Owner。现在,此选取器知道SingleComponentPickerViewController类的实例就
是他的数据源和委托,并且会要求该实例提供要显示的数据。
然后拖一个按钮到视图中。并连接操作。
7.5.3 将控制器实现为数据源和委托
在SingleComponentPickerViewController.m中,添加如下代码:
@synthesize singlePicker;
@synthesize pickerData;
- (IBAction)buttonPressed
{NSInteger row=[singlePicker selectedRowInComponent:0];
NSString *selected=[pickerData objectAtIndex:row];
NSString *title=[[NSString alloc] initWithFormat:@"You selected %@!",selected];
UIAlertView *alert=[[UIAlertView alloc] initWithTitle:title
message:@"Thank you for choosing."
delegate:nil
cancelButtonTitle:@"You're Welcome"
otherButtonTitles:nil];
[alert show];
[alert release];
[title release];
}
- (void)viewDidLoad {
NSArray *array=[[NSArray alloc]
initWithObjects:@"Luke",@"Leia",@"Han",@"Chewbacca",@"Artoo",@"Threepio",@"Lando",nil];
self.pickerData=array;
[array release];
}
- (void) dealloc {
[singlePicker release];
[pickerData release];
[super dealloc];
}
#pragma mark -
#pragma mark Picker Data Source Methods
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{ return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return [pickerData count];
}
#pragma mark Picker Delegate Methods
- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component
{
return [pickerData objectAtIndex:row];
}
@end
buttonPressed方法与日前选取器使用的对应方法几乎一样。与日期选取器不同,常规的选取器无法告诉我们他包含的数据,因为他
没有维护这些数据。他将此工作交由委托和数据源处理。我们需要询问选取器哪一行被选中,然后从pickerData数组提取相应的数
据。
NSInteger row=[singlePicker selectedRowInComponent:0];
0表示我们需要指定想要了解的组件,此选取器只有一个组件,因此我们传入0.这是第一个组件的索引。
注意:注意到NSInteger与row之间没有星号*了吗?在iPhone中,虽然前缀“NS”通常表示来自基础框架的Obj-C类,但此处是这个
一般规则的一个例外。NSInteger始终定义为整数数据类型,无论是int还是long。我们使用NSInteger,而没有使用int或long,因
为当使用NSInteger时,编译器将自动选择最适合目标平台的整数类型,当针对32位处理器编译时,编译器将创建一个32位int,当
针对64位体系结构编译时,他将创建一个64位long。目前还没有64位的iPhone,但是谁知道以后会不会有呢?也可以为iPhone应用
程序编写类 便于以后在面向Mac OS X的Cocoa应用程序中使用,Mac OS X能够同时支持32位和64位应用程序。
在viewDidLoad中,我们创建了一个包含几个对象的数组,用于向选取器提供数据。通常而言,数据来自于其他数据源,比如项目中
的Resources文件夹中的属性列表。利用此处的方式在代码中嵌入一组项,将难以更新此列表或将应用程序转换为其他语言。
下面是UIPickerViewDataSource协议的方法:
#pragma mark -
#pragma mark Picker Data Source Methods
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 1;
}
上面这个方法告诉选取器应该显示几个组件。这里只想显示1个组件,所以返回1.注意,UIPickerView将作为参数传入。此参数指向
询问此问题的选取器视图,这使同一个数据源能够控制多个选取器。
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return [pickerData count];
}
再一次,我们知道是哪个选取器正在询问问题,以及该选取器询问的是关于哪个组件的信息。
下面是委托方法:与数据源方法不同,所有委托方法都是“可选”的。术语“可选”具有一定的欺骗性,因为必须至少实现一个委
托方法。你通常会实现我们在此处实现的方法。在处理自定义选取器时,如果想要在选取器中显示除文本之外的其他内容,则必须
实现一个不同的方法:
#pragma mark Picker Delegate Methods
- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component
{
return [pickerData objectAtIndex:row];
}
好了,解释完了,就编译运行程序,看看效果吧!
7.6 实现多组件选取器
没有联系的多组件选取器和单组件选取器没有什么区别。再次不做过多描述
7.6.1 声明输出口和操作
#import <UIKit/UIKit.h>
#define kFillingComponent 0
#define kBreadComponent 1
@interface DoubleComponentPickerViewController:UIViewControoler <UIPickerViewDelegate,UIPickerViewDataSource> {
IBOutlet UIPickerView *doublePicker;
NSArray *fillingTypes;
NSArray *breakTypes;
}
@property (nonatomic,retain) UIPickerView *doublePicker;
@property (nonatomic,retain) NSArray *fillingTYpes;
@property (nonatomic,retain) NSArray *breakTypes;
- (IBAction)buttonPressed;
@end
7.6.2 构建视图
7.6.3 实现控制器
#import "DoubleComponetPickerViewController.h"
@implementation DoubleComponentPickerViewController
@synthesize doublePicker;
@synthesize fillingTypes;
@synthesize breadTypes;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleNorNil {
if (self=[super initWithNibName:nibNameOrNil bundle:nibBundleNorNil]) {
//Initialization code
}
return self;
}
- (IBAction)buttonPressed
{ NSInteger breadRow=[doublePicker selectedRowInComponent:kBreadComponent];
NSInteger fillingRow=[doublePicker selectedRowInComponent:kFillingComponent];
NSString *bread=[breadTypes objectAtIndex:breadRow];
NSString *filling=[fillingTypes objectAtIndex:fillintRow];
NSString *message=[[NSString alloc] initWithFormat:@"Your %@ on %@ bread will be right up.",filling,bread];
UIAlertView *alert=[[UIAlertView alloc] initWithTitle:@"Thank you for your order"
message:message
delegate:nil
cancelButtonTitle:@"Great"
otherButtonTitles:nil];
[alert show];
[alert release];
[message release];
}
- (void)viewDidLoad {
NSArray *breadArray=[[NSArray alloc] initWithObjects:@"White",@"Whole Wheat",@"Rye",@"Sourdough",@"Seven
Grain",nil];
self.breadTypes=breadArray;
[breadArray release];
NSArray *fillingArray=[[NSArray alloc] initWithObjects:@"Ham",@"Turkey",@"Peanut Butter",@"Tuna Salad",@"Chicken
Salad",@"Roast Beef",@"Vegemite",nil];
self.fillingTypes=fillingArray;
[fillingArray release];
}
- (void)dealloc {
[doublePicker release];
[breadTypes release];
[fillingTypes release];
[super dealloc];
}
#pragma mark -
#pragma mark Picker Data Source Methods
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 2;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
if (component==kBreadComponent}
return [self.breadTypes count];
return [self.fillingTYpes count];
}
#pragma mark Picker Delegate Methods
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{ if (component==kBreadComponent)
return [self.breadTypes objectAtIndex:row];
return [self.fillTypes objectAtIndex:row];
}
@end
7.7 实现独立组件
在本节中。新选取器将在左侧组件中显示一组美国的州,在右侧组件中显示与当前在左侧选定的州相对应的ZIP编码。
输出口和操作:
#import <UIKit/UIKit.h>
#define kStateComponent 0
#define kZipComponent 1
@interface DependentComponentPickerViewController:UIViewController <UIPickerViewDelegate,UIPickerViewDatasource> {
IBOutlet UIPickerView *picker;
NSDictionary *stateZips;
NSArray *states;
NSArray *zips;
}
@property (retain,nonatomic) UIPickerView *picker;
@property (retain,nonatomic) NSDictionary *stateZips;
@property (retain,nonatomic) NSArray *states;
@property (retain,nonatomic) NSArray *zips;
- (IBAction)buttonPressed;
@end
创建视图
实现控制器
数据源方法看起来与实现DoublePicker视图的方法几乎相同。所有增加的复杂度都在viewDidLoad和一个新的委托方法
pickerView:didSelectRow:inComponent:之间进行处理。
这里你肯定不希望输入数千个值来构建原始数组和NSDictionary,所以我们将从一个“属性列表”载入数据。前面已经提到,
NSArray和NSDictionary对象都可以通过属性列表创建。我们已经准备了一个属性列表statedictionary.plist。将项目归档文件的
07 Pickers文件夹内的这个文件导入到Xcode中并单击他,可以查看甚至编辑其中的数据。
#import "DependentComponentPickerViewController.h"
@implementation DependentComponentPickerViewController
@synthesize picker;
@synthesize stateZips;
@synthesize states;
@synthesize zips;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self=[super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
//Initialization code
}
return self;
}
- (IBAction)buttonPressed
{
//从略
}
- (void)ViewDidLoad {
NSBundle *bundle=[NSBundle mainBundle];
NSString *plistPath=[bundle pathForResource:@"statedictionary" ofType:@"plist"];
NSDictionary *dictionary=[[NSDictionary alloc] initWithContentsOfFile:plistPath];
self.stateZips=dictionary;
[dictionary release];
NSArray *components=[self.stateZips allKeys];
NSArray *sorted=[compnents sortedArrayUsingSelector:@selector(compare:)];
self.states=sorted;
NSString *selectedState=[self.states objectAtIndex:0];
NSArray *array=[stateZips objectForKey:selectedState];
self.zips=array;
}
- (void)dealloc {
[picker release];
[stateZips release];
[states release];
[zips release];
[super dealloc];
}
#pragma mark -
#pragma mark Picker Data Source Methods
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 2;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
if (component==kStateComponent}
return [self.states count];
return [self.zips count];
}
#pragma mark Picker Delegate Methods
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{ if (component==kStateComponent)
return [self.states objectAtIndex:row];
return [self.zips objectAtIndex:row];
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
if (component==kStateComponent) {
NSString *selectedState=[self.states objectAtIndex:row];
NSArray *array=[stateZips objectForKey:selectedState];
self.zips=array;
[picker selectRow:0 inComponent:kZipComponent animated:YES];
[picker reloadComponent:kZipComponent];
}
}
@end
在新的viewDidLoad方法中,我们做的第一件事情就是提取对应用程序的主束(main bundle)的引用。
NSBundle *bundle=[NSBundle mainBundle];
那么什么是束呢?“束”只是一种特定的文件类型,其中的内容遵循特定的结构。应用程序和框架都是束,此调用返回的束对象表
示我们的应用程序。NSBundle的一个主要作用是获取添加到项目的Resources文件夹的资源。在构建应用程序时,这些文件将被复制
到应用程序的束中。我们已经在项目中添加了图像等资源,但是到现在为止,我们还只是在IB中使用他们。如果想要在代码中使用
这些资源,则必须使用NSBundle。
NSString *plistPath=[bundle pathForResource:@"statedictionary" ofType:@"plist"];
这将返回一个字符串,其中包含statedictionary.plist文件的位置。然后可以使用该路径创建一个i额NSDictionary对象。当这样
做时,属性列表的所有内容将被载入到新创建的NSDictionary对象中。然后,将该对象分配给stateZips。
。。。
新的委托方法是这个示例的核心所在:
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
if (component==kStateComponent) {
NSString *selectedState=[self.states objectAtIndex:row];
NSArray *array=[stateZips objectForKey:selectedState];
self.zips=array;
[picker selectRow:0 inComponent:kZipComponent animated:YES];
[picker reloadComponent:kZipComponent];
}
}
只要选取器的选择发生变化,就会调用这个方法pickerView:didSelectRow:inComponent:
我们的工作还没有完成。编译并运行应用程序,检查Dependent标签。是否存在不合意的地方?两个组件大小相同。显示左侧显示州
的组件的宽度应该更宽一些。这里可以实现另一个委托方法来指定每个组件应该占用的宽度。在纵向模式下,选取器组件的可用宽
度大概是295px,但是对于添加的每个附加组件。可能没有空间绘制新组件的边缘。也许需要调整组件的值以获得最佳的显示效果。
将以下方法添加到DependentComponentPickerViewController.m的委托部分:
- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component
{ if (component==kZipComponent)
return 90;
return 200;//以像素为单位
}
选取器将尽可能适应这个宽度值。
7.8 使用自定义选取器创建简单游戏
现在,我们将创建一个实际的老虎机游戏。
7.8.1 编写控制器头文件 CustomPickerViewController.h
#import <UIKit/UIKit.h>
@interface CustomPickerViewController:UIViewController <UIPickerViewDatasource,UIPickerViewDelegate>
{
IBOutlet UIPickerView *picker;
IBOutlet UILable *winLabel;
NSArray *column1;
NSArray *column2;
NSArray *column3;
NSArray *column4;
NSArray *column5;
}
@property (nonatomic.retain) UIPickerView *picker;
@property (nonatomic.retain) UILabel *winLabel;
@property (nonatomic.retain) NSArray *column1;
@property (nonatomic.retain) NSArray *column2;
@property (nonatomic.retain) NSArray *column3;
@property (nonatomic.retain) NSArray *column4;
@property (nonatomic.retain) NSArray *column5;
- (IBAction)spin;
@end
5个NSArray用于保存图像视图,这些视图包含我们想要选取器绘制的图像。虽然所有5列使用相同的图像,但我们仍然需要将每列对
应的数组分开,使其拥有自己的图像视图集,因为每个视图只能在选取器中的一个位置绘制。
7.8.2 构建视图
构建视图其实和前面没什么区别。打开CustomPickerView.xib。添加一个i额标签和一个按钮。按钮标题命名为Spin。然后选定标签
,在Fonts(Cmd+T)面板中调整标签文本和外观并将字体放大。可能还需要将标签本身放大,以适应新的大文本的大小。也可以使
用属性检查器为标签分配一个漂亮的颜色。设置字体时,确保将文本设置为中间对齐。得到想要的文本之后,删除文字Label,因为
我们不想在用户首次胜利之前显示任何文本。
然后建立所有到输出口和操作的连接。
最后,还有一件事情需要做。选择选取器并打开属性检查器。需要取消选中User Interaction Enabled的复选框,这样,用户就不
能够手动更改刻度盘进行作弊了。
注意:在IB中设计iPhone界面时,一定要小心使用Fonts面板。IB中允许将Mac上的任何字体分配给标签。但是iPhone支持的字体类
型很少。应该将字体选择限制为以下字体系列:American Typewriter、AppleGothic、Arial、Arial Rounded MT Bold、Arial。。
7.8.3 添加图像资源
我们在项目归档文件07 Pickers/Custom Picker Images文件夹中包含了6个图像文件(seven.png、bar.png、crown.png、
cherry.png、lemon.png和apple.png)。将所有这些文件添加到Resources文件夹。在提示是否创建副本时,最后将这些文件复制到
项目文件夹中。
7.8.4 实现控制器
在这个控制器的实现过程中,我们添加了许多新内容。
#import "CustomPickerViewController.h"
@implementation CustomPickerViewController
@synthesize picker;
@synthesize winLabel;
@synthesize column1;
@synthesize column2;
@synthesize column3;
@synthesize column4;
@synthesize column5;
- (IBAction)spin
{ BOOL win=NO;
int numInRow=1;
int lastVal=-1;
for (int i=0;i<5;i++)
{int newValue=random() % [self.column1 count];
if (newValue==lastVal)
numInRow++;
else
numInRow=1;
lastVal=newValue;
[picker selectRow:newValue inComponent:i animated:YES];
[picker reloadComponent:i];
if (numInRow>=3)
win=YES;
}
if (win)
winLabel.text=@"WIN!";
else
winLabel.text=@"";
}
- (void)vieDidLoad{
UIImage *seven=[UIImage imageNamed:@"seven.png"];
UIImage *bar=[UIImage imageNamed:@"bar.png"];
UIImage *cherry=[UIImage imageNamed:@"cherry.png"];
UIImage *crown=[UIImage imageNamed:@"crown.png"];
UIImage *lemon=[UIImage imageNamed:@"lemon.png"];
UIImage *apple=[UIImage imageNamed:@"apple.png"];
for (int i=1;i<=5;i++)
{ UIImageView *sevenView=[[UIImageView alloc] initWithImage:seven];
UIImageView *barView=[[UIImageView alloc] initWithImage:bar];
UIImageView *crownView=[[UIImageView alloc] initWithImage:crown];
UIImageView *cherryView=[[UIImageView alloc] initWithImage:cherry];
UIImageView *lemonView=[[UIImageView alloc] initWithImage:lemon];
UIImageView *appleView=[[UIImageView alloc] initWithImage:apple];
NSArray *imageViewArray=[[NSArray alloc] initWithObjects:sevenView,barView,crownView,cherryView,lemonView,appleView,nil];
NSString *fieldName=[[NSString alloc] initWithFormat:@"column%d",i];
[self setValue:imageViewArray forKey:fieldName];
[fieldName release];
[imageViewArray release];
[sevenView release];
[barView release];
[crownView release];
[cherryView release];
[lemonView release];
[appleView release];
}
srandom(time(NULL));
}
#pragma mark -
#pragma mark Picker Data Source Methods
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{ return 5;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{ return [self.column1 count];
}
#pragma mark Picker Delegate Methods
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
NSString *arrayName=[[NSString alloc] initWithFormat:@"column%d",component+1];
NSArray *array=[self valueForKey:arrayName];
return [array objectAtIndex:row];
}
@end
7.8.5 spin方法
7.8.6 viewDidLoad方法
我们做的第一件事情是载入6个不同的图像。UIImage类提供的imageNamed:便利方法可以轻松完成此任务
UIImage *seven=[UIImage imageNamed:@"seven.png"];
UIImage *bar=[UIImage imageNamed:@"bar.png"];
UIImage *cherry=[UIImage imageNamed:@"cherry.png"];
UIImage *crown=[UIImage imageNamed:@"crown.png"];
UIImage *lemon=[UIImage imageNamed:@"lemon.png"];
UIImage *apple=[UIImage imageNamed:@"apple.png"];
前面已经提到过,在使用便捷的类方法初始化对象时需要谨慎,因为他们使用了自动释放池。但是这里是一个例外,原因有两点;
1)这段代码仅在应用程序启动时触发
2)他非常方便使用。通过使用此方法,无需在iPhone上确定每个图像的位置,然后使用该信息来载入每个图像。这可以节省数十行代码,而且不会增加太多的内存开销。
然后是创建UIImageView实例,分别对象每个图像以及5个选取器组件中的每一个。我们通过循环来完成此任务。
for (int i=1;i<=5;i++)
{ UIImageView *sevenView=[[UIImageView alloc] initWithImage:seven];
UIImageView *barView=[[UIImageView alloc] initWithImage:bar];
UIImageView *crownView=[[UIImageView alloc] initWithImage:crown];
UIImageView *cherryView=[[UIImageView alloc] initWithImage:cherry];
UIImageView *lemonView=[[UIImageView alloc] initWithImage:lemon];
UIImageView *appleView=[[UIImageView alloc] initWithImage:apple];
创建了图像视图之后,将这些视图放到一个数组中。此数组将用于向选取器的每个组件提供数据。
NSArray *imageViewArray=[[NSArray alloc] initWithObjects:sevenView,barView,crownView,cherryView,lemonView,appleView,nil];
现在,只需将此数组分配给5个数组之一。谓词,我们将创建一个字符串,该字符串与一个数组的名称匹配。
NSString *fieldName=[[NSString alloc] initWithFormat:@"column%d",i];
然后使用一个非常方便的setValue:forKey:方法将此数组分配给该属性。
[self setValue:imageViewArray forKey:fieldName];
然后进行内存清理:
[fieldName release];
[imageViewArray release];
[sevenView release];
[barView release];
[crownView release];
[cherryView release];
[lemonView release];
[appleView release];
}
此方法的最后一项任务是提供一个随机数生成器。如果不提供随机数生成器,则每次游戏的结构都是一样的,这样就失去了游戏的意义了。
srandom(time(NULL));
向这5个数组填充了图像视图之后,我们还能够对她做什么呢?查看数据源方法,和前面版本几乎一样。查看委托方法,你会发现我们使用了一个完全不同的委托方法来向选取器提供数据。这个方法返回的是UIView *。
使用此方法,我们可以为选取器提供任何能够在UIView中绘制的内容。当然,由于选取器的尺寸较小,所以既能够正常工作又比较没挂你的内容少之又少。但是此方法使我们在选择显示的内容上拥有更多自由,只是需要更多的工作。
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
此方法返回5个数组中的某个图像视图。谓词,我们再次使用某个数组的名称创建一个NSString。由于Component的索引为0,所以我们将Component+1.
NSString *arrayName=[[NSString alloc] initWithFormat:@"column%d",component+1];
然后使用valueForKey:检索该数组。
NSArray *array=[self valueForKey:arrayName];
return [array objectAtIndex:row];
}
7.8.7 最后的细节
第一个他没有声音
第二个刻度盘旋转还未结束,游戏就告诉我们获胜了
1)首先解决声音问题:本书附带的项目归档文件的07 Pickers/Custom Picker Sounds文件夹包含两个声音文件:crunch.wav和win.wav。将这两个文件添加到Resources文件夹。分别用在用户点击旋转按钮和获胜时播放。
要添加以上声音,我们需要访问iPhone的Audio Toolbox类。在CustomPickerViewController.m中的顶部插入下面这行代码:
#import <AudioToolbox/AudioToolbox.h>
接下来,我们添加一个输出口,该输出口将指向旋转按钮。当滚轮滚动时,我们将隐藏该按钮。我们不希望在当前的旋转过程完成之前让用户再次点击该按钮。将以下代码添加到CustomPickerViewController.h:
IBOutlet UIButton *button;
并连接输出口。
我们还需要向控制器类添加两个方法。将以下两个方法添加到CustomPickerViewController.m,做为该类的前两个方法:
- (void)showButton
{ button.hidden=NO;
}
- (void)playWinSound
{ NSString *path=[[NSBundle mainBundle] pathForResource:@"win" ofType:@"wav"];
SystemSoundID soundID;
AudioServiceCreateSystemSoundID((CFURLRef)[NSURL fileURLWithPath:path],&soundID);
AudioServicePlaySystemSound(soundID);
winLabel.text=@"WIN";
[self performSelector:@selector(showButton) withObject:nil afterDelay:1.5];
}
第一个方法用于显示按钮
第二个方法将在用户获胜之后被调用。这里我们使用一个名为performSelector:withObject:afterDelay:的方法,通过一种特殊方法调用显示按钮方法。所有对象都可以非常方便地使用此方法,他运行在未来某个时候调用该方法。在本例中,将在1.5s之后调用该方法,这会使游戏在刻度盘旋转到最终位置之后才告诉用户结果。;
我们还需要为spin:方法进行一些修改。需要编写代码来播放声音,并在游戏者获胜之后调用playerWon方法。
- (IBAction)spin
{ BOOL win=NO;
int numInRow=1;
int lastVal=-1;
for (int i=0;i<5;i++)
{int newValue=random() % [self.column1 count];
if (newValue==lastVal)
numInRow++;
else
numInRow=1;
lastVal=newValue;
[picker selectRow:newValue inComponent:i animated:YES];
[picker reloadComponent:i];
if (numInRow>=3)
win=YES;
}
//新添加的内容
button.hidden=YES;
NSString *path=[[NSBundle mainBundle]pathForResource:@"crunch" ofType:@"wav"];
SystemSoundID soundID;
AudioServiceCreateSystemSoundID((CFURLRef)[NSURL fileURLWithPath:path],&soundID);
AudioServicePlaySystemSound(soundID);
if (win)
[self performSelector:@selector(playWinSound) withObject:nil afterDelay:.5];
else
[self performSelector:@selector(showButton) withObject:nil afterDelay:.5];
winLabel.text=@"";
}
7.8.8 链接Audio Toolbox框架
如果现在尝试编译应用程序,将会得到另一个链接错误。事实表明,这个错误与用于载入和播放声音的函数有关。按住Command并双击AudioServiceCreateSystemSoundID函数,这将转到声明该函数的头文件。在该文件中,我们可以看到此函数是Audio Toolbox框架的一部分。
从Project=》Add to Project...,然后导航到iPhone仿真器的框架文件夹,位于
/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator2.1.sdk/System/Library/Frameworks/
中,并将AudioToolbox.framework添加到项目中,确保没有将该框架复制到项目中,并选择Relative to Current SDK。