三十而立,从零开始学ios开发(十一):Tab Bars和Pickers

不好意思各位,本人休息了一个礼拜,所以这次的进度延后了,而且这次的学习的内容比较多,时间用的也比较长,文章发布的时间间隔有些长了,望各位谅解,下面继续我们的ios之旅。

这次我们主要学习的内容有2个,一个是Tab Bar,如下图

很熟悉的界面(iphone中的phone),另一个Picker,如下图

在正是开始学习项目之前,先首先简单介绍一下这次的例子的一个结构,当然一个root controller肯定是有的,用来控制其他subController的切换,在root controller中会放置一个tab bar,这个tab bar中有5个item,分别对应5个不同的view,每个view中有一个picker,所以在这个例子中,会有5个功能不同的picker,基本上涵盖了日常可能会用到的常规情况。OK,下面开始这次的学习。

1)创建一个工程,选择Empty Application,并命名为Pickers
这个过程和前一篇是一样的,在这里就不再叙述了。

2)添加需要的文件
之前我们说过,这个项目会有5个subcontrollers,因此我们需要添加5个view,选中Project navigator中的Pickers文件夹,单击鼠标右键,选择“New File...”,或者使用快捷键command+N,或者使用菜单栏File>New>New File...
 

在弹出的窗口中,左边选择Cocoa Touch,右边选择UIViewController subclass,点击Next按钮。

在下一个窗口中,填写类名BIDDatePickerViewController,并确认“With XIB for user interface” checkbox被选中(在前一篇的学习中,我们没有选中这个checkbox,而是在之后手动的连接controller和xib文件,在这里系统将帮我们连接这2个文件),点击Next

选择保持的位置,保存在“Pickers”目录下,完成创建。创建完成后的Project navigator如下

一共有3个文件被创建,分别是.h、.m和.xib。

使用相同的方法再创建其他4个subcontroller,分别命名为BIDSingleComponentPickerViewController、BIDDoubleComponentPickerViewController、BIDDependentComponentPickerViewController、BIDCustomPickerViewController。创建完成后的Project navigator如下:

其中:
BIDDatePickerViewController:包含一个Date Picker
BIDSingleComponentPickerViewController:包含一个Picker View,并有一个滚轴1一个component)
BIDDoubleComponentPickerViewController:包含一个Picker View,并有两个滚轴(2个component),且这2个滚轴是独立的
BIDDependentComponentPickerViewController:包含一个Picker View,并有两个滚轴(2个component),且这2个滚轴是联动的
BIDCustomPickerViewController:包含一个Picker View,并有五个滚轴(5个component),这5个滚轴中的内容是图片

(温馨提示:每一步做完后,你可以先build一下你的code,看看有没有错误,有的话可以及时修改,有一些警告应该是正常的,我们并没有完全完成项目的创建,但是不应该报错。)

3)添加Root View Controller(就是前面的提到的root controller)
添加root controller选择的模板和前面的有些不同,选中Project navigator中的Pickers文件夹,使用快捷键command+N创建一个新文件,在弹出的选中模板对话框中,左边选择User Interface,右边选择Empty模板,单击Next

Device Family保持默认的iPhone,点击Next,将新建的模板命名为TabBarController.xib,点击Create,创建完成。然后一个单独的TabBarController.xib文件会出现在Project navigator中。(Empty模板不会帮我们创建任何.h.m文件,我们会将TabBarController.xib关联到BIDAppDelegate,因此我们不需要和前面的文件一样创建.h.m文件,其次Empty模板不会帮我们创建一个默认的View,点击TabBarController.xib,你会发现xib的窗口中什么东西也没有,我们需要在之后自己添加)

打开BIDAppDelegate.h文件,添加以下内容

#import <UIKit/UIKit.h>

@interface BIDAppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) IBOutlet UITabBarController *rootController;

@end

 打开BIDAppDelegate.m文件,添加以下内容

#import "BIDAppDelegate.h"

@implementation BIDAppDelegate

@synthesize window = _window;
@synthesize rootController;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    
    [[NSBundle mainBundle] loadNibNamed:@"TabBarController" owner:self options:nil];
    [self.window addSubview:rootController.view];
    
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

...

这些方法应该比较熟悉了,和之前几篇的例子很相似。

在此打开TabBarControll.xib,在Object library中找到Tab Bar Controller

并拖动到xib窗口

下面的一步是将TabBarControll.xib关联到BIDAppDelegate,选中TabBarControll.xib,然后选择File's Owner,打开identity inspector,Class中选择BIDAppDelegate

接着打开connections inspector,在Outlets中会出现我们刚在定义在BIDAppDelegate.h中的rootController,鼠标放到rootController右边的圆圈上,圆圈中会出现加号,然后点击鼠标拖动到xib窗口中的Tab Bar Controller上,将2者关联起来,之后在rootController边上就会显示Tab Bar Controller了。

4)添加Tab Bar Item
首先可以看一下Tab Bar Controller的结构

一个Tab Bar Controller下面有一个Tab Bar,然后这个Tab Bar下有很多Item(每个Tab Bar Controller默认会有2个tab bar item),每个Item有一个View Controller,每个View Controller下又有一个Tab Bar Item,好了这个就是一个Tab Bar Controller的基本结构,因为我们需要5个tab bar item,下面就添加其他的3个。

在Object Library中找到Tab Bar Item

然后拖动到Tab Bar上,一共拖3个,位置无所谓,添加完tab bar item的结构如下
 
这2者是一一对应的。

5个tab bar item分别对应5个view,因此我们需要制定2者的对应关系,选中最左边的一个tab bar item,打开attribute inspector,在NIB Name的下拉列表中选择BIDDatePickerViewController,然后打开identity inspcetor,在Class中选择BIDDatePickerViewController(在NIB Name中选择BIDDatePickerViewController是指定tab bar item对应的view是哪个,在Class中选中BIDDatePickerViewController是指定view的delegate是哪个类),对其他的4个tab bar item都执行上面的操作,分别对应各自的controller。从左到右的顺序为:
BIDDatePickerViewController
BIDSingleComponentPickerViewController
BIDDoubleComponentPickerViewController
BIDDependentComponentPickerViewController
BIDCustomPickerViewController

5)为Tab Bar Item添加icon
下载TabBarIcons,并其解压后连同文件夹一同拖入到Project navigator中,在弹出的“Choose options for adding these files”框中保持默认选项即可,添加完后的Project navigator如下

一般来说,app中图片基本上都是使用png格式,然后Tab Bar Item的图片大小为24px * 24px。(感觉xcode还是很只能的,它能够自己搜索所有的在项目中的图片文件,然后供用户进行选择,无论你图片放在那个文件夹下,xcode都可以找到)

图片拖动到项目中后,接下来就是为tab bar item添加图片了,首先选中最左边的一个Tab Bar Item,选中的方法是在视图中点击2次最左边的item图标,第一次我们选中的是view controller,第二次才是真正的tab bar item
 
第二次选中后,打开Attributes inspector,在Bar Item的Image中找到clockicon.png,这样icon就设好了,然后将Title命名为Date即可。

其他的四个item都是相同的操作,对应关系如下
BIDDatePickerViewController(clockicon.png,Title:Date)
BIDSingleComponentPickerViewController(singleicon.png,Title:Single)
BIDDoubleComponentPickerViewController(doubleicon.png,Title:Double)
BIDDependentComponentPickerViewController(dependenticon.png,Title:Dependent)
BIDCustomPickerViewController(toolicon.png,Title:Custom)
完成后的样子

至此,我们对于root controller的所有设置都完成了,编译运行一下程序,看看效果,之后在进行每个subcontroller的实现。

6)Date Picker
第一个实现的是Date Picker,首先打开BIDDatePickerViewController.h,添加如下代码

#import <UIKit/UIKit.h>

@interface BIDDatePickerViewController : UIViewController

@property (strong, nonatomic) IBOutlet UIDatePicker *datePicker;
- (IBAction)buttonPressed;

@end

声明一个UIDatePicker的Outlet,用于等会和DatePicker控件连接,然后声明一个Action事件,我们会添加一个button,会触发该事件。

接着打开BIDDatePickerViewController.xib,然后在Graphical Layout Area(简称GLA)中选中View,再打开Attributes inspector,在Simulated Metrics中找到Bottom Bar,并选择“Tab Bar”,如下

设置完成后,在GLA中会出现一条黑色区域,表示Tab Bar area,这样这个View的实际大小会把Tab Bar area给去掉,实际的高度变成411px

在Object library中找到Date Picker

并拖动到View的顶部,接着拖一个button放在DatePicker的下方,并命名为Select,完成后的效果如下

我们还需要一个设置,设置DatePicker的最大日期和最小日期,选中View上的DatePicker,然后打开Attributs inspector,在Date Picker栏找到Constraints,将2个checkbox勾上

(其他的设置选项你自己也可以随便玩玩,看看DatePicker有什么变化)

7)连接Outlet和Action,写code
选中View上的button,打开connections inspector,在Sent Events中找到Touch Up Inside,将其连接到File's Owner下的buttonPressed action
选中File's Owner,control-drag到View中的DatePicker上,选择Outlet:datePicker

好了,所有的连接都完成了,下面就可以写code了

打开BIDDatePickerViewController.m,添加如下code

@implementation BIDDatePickerViewController
@synthesize datePicker;

......

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    NSDate *now = [NSDate date];
    [datePicker setDate:now animated:NO];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.datePicker = nil;
}

......

- (IBAction)buttonPressed
{
    NSDate *selected = [datePicker date];
    NSString *message = [[NSString alloc] initWithFormat:@"The date and time you selectd is: %@", selected];
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Date and Time Selected"
                                                    message:message
                                                   delegate:self
                                          cancelButtonTitle:@"Yes, I do"
                                          otherButtonTitles:nil];
    [alert show];
}

这些code应该比较容易理解,在viewDidLoad中初始化datePicker显示的日期,在viewDidUnload中释放datePicker,在buttonPressed方法中,首先获取datePicker当前选中的时间对象NSDate,然后赋给一个string,最后alert显示出来,非常简单。

编译运行code

点击Select按钮,一个alert框弹出

一切正常唯有时间不对,iphone上方显示的是11:13 AM,但是alert中显示的时间是03:11:13 +0000,这是它显示的是格林尼治时间,和中国的时差有8个小时,要解决这个问题,改写buttonPressed如下

- (IBAction)buttonPressed
{
    NSDate *selected = [datePicker date];
    
    NSTimeZone *zone = [NSTimeZone systemTimeZone];
    NSInteger interval = [zone secondsFromGMTForDate: selected];
    NSDate *localeDate = [selected  dateByAddingTimeInterval: interval]; 
    NSString *message = [[NSString alloc] initWithFormat:@"The date and time you selectd is: %@", loaclDate];
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Date and Time Selected"
                                                    message:message
                                                   delegate:self
                                          cancelButtonTitle:@"Yes, I do"
                                          otherButtonTitles:nil];
    [alert show];
}

NSTimeZone *zone = [NSTimeZonesystemTimeZone]; // 获得当前设备的时区,即iphone的时区

NSInteger interval = [zone secondsFromGMTForDate: selected]; // 算出当前时区和GMT的时差是多少

NSDate *localeDate = [selected  dateByAddingTimeInterval: interval]; // 在GMT的基础上加上加上相应的时差

好了,在此编译运行,点击button后,显示的时间和iphone上的一致了

8)Single-Component Picker
DatePicker是一个特殊的picker,专门用于对日期进行选择,但是在很多时候,我们需要选择的内容都是需要我们自己制定的,在这个例子中,我们就将制定自己内容。
在这里例子中,我们会添加一个Picker View

在Picker中,每一个滚轴就是一个component,我们将使用delegate来定义Picker View中component的数量和数据的来源。

9)创建Outlet和Action
打开BIDSingleComponentPickerViewController.h,添加如下code

#import <UIKit/UIKit.h>

@interface BIDSingleComponentPickerViewController : UIViewController <UIPickerViewDelegate, UIPickerViewDataSource>

@property (strong, nonatomic) IBOutlet UIPickerView *singlePicker;
@property (strong, nonatomic) NSArray *pickerData;

- (IBAction)buttonPressed;

@end

首先添加了2个protocols,这2个protocols是必须的,PickerView的delegate和datasource都是源于它,然后声明了一个指向PickerView的对象,再声明一个保存PickerView数据的数列,最后声明了一个action,代码很简单。

10)添加Picker View,绑定Outlet和Action
选中BIDSingleComponentPickerViewController.xib,空出底部的Tab bar位置(Attributes inspector>Simulated Metrics>Bottom Bar>Tab Bar。在Object library中,找到Picker View,拖动到View的顶部,在拖一个button放在Picker View的下面,命名为Select,所有控件添加完毕

选中File's Owner,control-drag到Picker View上,选中singlePicker。
选中Picker View,打开connections inspector,拖动dataSource和delegate边上的圆圈到File's Owner上进行绑定,这样就告诉这个Picker View,它的dataSource和delegate都是BIDSingleComponentPickerViewController类

绑定Select按钮的Touch Up Inside的action为buttonPressed(方法和之前的一个例子一样)

ok,至此所有的控件和连接操作都已经完成,下面开始code部分。

11)写code
打开BIDSingleComponentPickerViewController.m,添加如下代码

#import "BIDSingleComponentPickerViewController.h"

@implementation BIDSingleComponentPickerViewController
@synthesize singlePicker;
@synthesize pickerData;

......

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    NSArray *array = [[NSArray alloc] initWithObjects:@"Luke", @"Leia",
                      @"Han", @"Chewbacca", @"Artoo", @"Threepio", @"Lando", nil];
    self.pickerData = array;
}

- (void)viewDidUnload
{
    [self setSinglePicker:nil];
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.singlePicker = nil;
    self.pickerData = nil;
}

......

- (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];
}

一些必要的解释,在viewDidLoad中,声明一个数组,存放在PickerView的componnent中显示的数据。
buttonPressed方法中:
NSInteger row = [singlePicker selectedRowInComponent:0]; // selectedRowInComponent方法返回当前component滚到哪一行,后面的0表示最左边的一个component
NSString *selected = [pickerData objectAtIndex:row]; // objectAtIndex:row返回component中当前行的对象,这个例子中是NSString

其他的代码都比较简单,下面继续添加code,这次添加dataSource和delegate需要使用到的方法,这也是每个Picker View(不包括Date Picker)都需要实现的方法

#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];
}

numberOfComponentsInPickerView告诉picker一共有几个component(滚轴),我们这个例子是SingleComponent,只有一个滚轴,因此返回1。这个方法有一个参数是传一个pickerview进来,如果一个view中有2个或2个以上的Picker View,就需要使用这个参数,告诉系统到底是在设置哪个Picker View的component的数量,如果只有1个Picker View就可以忽略该参数,我们这个例子就忽略了这个参数。

pickerView方法是返回dataSource的数量,告诉pick你有多少个数据需要显示。参数pickerView指明是哪个picker在调用,只有一个的话可以忽略该参数,参数component是picker中的第几个滚轴,如果只有一个滚轴,也可以忽略该参数。

以上是的2个方法都是属于dataSource,最后的一个方法是属于delegate的,Picker View的dataSource方法是必须实现的,而delegate方法是可选的,但必须要实现其中的一个,在这个例子中我们实现的是pickerView方法
pickerView方法在指定的picker和component中返回第row行数据,这个例子中的数据都来自NSArray,就返回array中的第row个数据即可。在这个方法中,只用到了参数row,其他2个参数不需要,因为我们只有1个pickerview和1个component。

至此,所有的Single component的所有code都写完了,编译运行,选择Tab Bar上的第二个item

单击Select按钮,警告框弹出,显示哪一行的内容被选中了

13)Multicomponent Picker
在这个例子中,将会有2个component,它们各自独立(没有关联),实现起来也很简单,只要准备2个NSArray,在返回component的数量的方法中返回2就可以了,下面进行具体的实现。

14)创建Outlet和Action
打开BIDDoubleComponentPickerViewController.h,添加如下code

#import <UIKit/UIKit.h>

#define kfillingComponent 0
#define kBreadComponent 1

@interface BIDDoubleComponentPickerViewController : UIViewController <UIPickerViewDelegate, UIPickerViewDataSource>

@property (strong, nonatomic) IBOutlet UIPickerView *doublePicker;
@property (strong, nonatomic) NSArray *fillingTypes;
@property (strong, nonatomic) NSArray *breadTypes;

- (IBAction)buttonPressed;

@end

上面的code应该很容易理解,定义了2个宏变量来区分左右2个component,这是一个很好的习惯,而且使代码更易读,然后添加了2个protocols,声明了一个指向PickerView的outlet,声明2个数组,分别保存左右2个component的数据,最后定义一个action,很简单。

15)添加Picker View,绑定Outlet和Action
打开BIDDoubleComponentPickerViewController.xib,设置view的Bottom Bar为Tab Bar,拖一个Picker View放到view的顶部,拖一个按钮放到Picker View的下面,并命名为Select,完成后的界面

选中File's Owner,control-drag到Picker View上,选中doublePicker。
选中Picker View,打开connections inspector,拖动dataSource和delegate边上的圆圈到File's Owner上进行绑定,这样就告诉这个Picker View,它的dataSource和delegate都是BIDDoubleComponentPickerViewController类
绑定Select按钮的Touch Up Inside的action为buttonPressed

16)写code
打开BIDDoubleComponentPickerViewController.m文件,添加如下代码

@implementation BIDDoubleComponentPickerViewController
@synthesize doublePicker;
@synthesize fillingTypes;
@synthesize breadTypes;

- (IBAction)buttonPressed
{
    NSInteger fillingRow = [doublePicker selectedRowInComponent:kfillingComponent];
    NSInteger breadRow = [doublePicker selectedRowInComponent:kBreadComponent];
    
    NSString *filling = [fillingTypes objectAtIndex:fillingRow];
    NSString *bread = [breadTypes objectAtIndex:breadRow];
    
    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];
}

......

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    NSArray *fillingArray = [[NSArray alloc] initWithObjects:@"Ham",
                             @"Turkey", @"Peanut Butter", @"Tuna Salad",
                             @"Nutella", @"Roast Beef", @"Vegemite", nil];
    self.fillingTypes = fillingArray;
    
    NSArray *breadArray = [[NSArray alloc] initWithObjects:@"White",
                           @"Whole Wheat", @"Rye", @"Sourdough", @"Seven Grain",nil];
    self.breadTypes = breadArray;

}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.doublePicker = nil;
    self.fillingTypes = nil;
    self.breadTypes = nil;
}

......

#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.fillingTypes objectAtIndex:row];
}

上面的code和在singlepicker中的基本一样,由于有2个component,因此在viewDidLoad中创建了2个数组,在numberOfComponentsInPickerView中返回2,这样PickerView就会创建2个component(滚轴),在DataSource的pickerView方法中,根据component来返回数组,在delegate的pickerView中根据component返回数据。很简单,然后编译运行程序,载入app后,选择Tab Bar上的第三个tab item,出现如下界面

随便滚动2个component,他们是独立的,不会相互影响

点击Select按钮,一个警告框弹出,告知你选择了哪些数据

17)Dependnet Components
前面的2个例子分别实现了1个componet和2个component,在这个例子中,也将实现2个component,但是这2个componet是联动的,当左边的component存放美国的各个洲(state),右边的component存放每个洲的邮政编码(zip),当左边的洲发生时,右边的邮政编码也会随之变化。另外在之前的例子中,显示在component中的数据都是写在code中的,这个不利于数据的更新,且如果数据量巨大,我们也不可能都写在code里面,在这个例子中,数据将存入一个property list(plist)文件,component的数据将从该文件中获取。

打开BIDDependentComponentPickerViewController.h文件,添加如下code

#import <UIKit/UIKit.h>
#define kStateComponent 0
#define kZipComponent 1 @interface BIDDependentComponentPickerViewController : UIViewController <UIPickerViewDelegate, UIPickerViewDataSource> @property (strong, nonatomic) IBOutlet UIPickerView *picker; @property (strong, nonatomic) NSDictionary *stateZips; @property (strong, nonatomic) NSArray *states; @property (strong, nonatomic) NSArray *zips; - (IBAction)buttonPressed; @end

唯一不同的是,多了一个NSDictionary对象,这个对象是用来存放关联数据的,存放之前提到的plist。

和前面2个例子一样,拖动一个Picker View和button到View上,并连接Outlet和Action

18)NSDictionary:statedictionary.plist
下载statedictionary.plist压缩包,并解压出里面的文件statedictionary.plist,将statedictionary.plist拖入到Project navigator中,放到Pickers目录下,点击statedictionary.plist文件,你会发现有茫茫多的数据

其实statedictionary.plist是一个xml文件,理解有序的将这些数据组织起来,供程序使用

19)写code
由于这次的code稍微有些不同,主要是在读取plist上面,因此我们分开写,打开BIDDependentComponentPickerViewController.m文件,首先添加如下code

#import "BIDDependentComponentPickerViewController.h"

@implementation BIDDependentComponentPickerViewController
@synthesize picker;
@synthesize stateZips;
@synthesize states;
@synthesize zips;

- (IBAction)buttonPressed
{
    NSInteger stateRow = [picker selectedRowInComponent:kStateComponent];
    NSInteger zipRow = [picker selectedRowInComponent:kZipComponent];
    
    NSString *state = [states objectAtIndex:stateRow];
    NSString *zip = [zips objectAtIndex:zipRow];
    
    NSString *title = [[NSString alloc] initWithFormat:@"You select zip code %@.", zip];
    
    NSString *message = [[NSString alloc] initWithFormat:@"%@ is in %@", zip, state];
    
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
                                                    message:message
                                                   delegate:nil
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
}

上面的code应该不需要过多的解释了吧,很简单,应该都可以看懂,接着添加viewDidLoad

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    NSBundle *boundle = [NSBundle mainBundle];
    NSURL *plistURL = [boundle URLForResource:@"statedictionary" withExtension:@"plist"];
    
    NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfURL:plistURL];
    
    self.stateZips = dictionary;
    
    NSArray *components = [self.stateZips allKeys];
    NSArray *sorted = [components sortedArrayUsingSelector:@selector(compare:)];
    self.states = sorted;
    NSString *selectedState = [self.states objectAtIndex:0];
    NSArray *array = [stateZips objectForKey:selectedState];
    self.zips = array;
}

这里面的变化比较大,首先声明了一个NSBundle对象,这个对象在前几篇的例子中已经出现过(load一个xib),这里做一个比较详细的解释:bundle是一个特殊的文件夹,其中包含了程序会使用到的资源,这些资源包含了如图像、声音、编译好的代码、xib文件。对应bundle,cocoa提供了类NSBundle。

下面逐个对语句进行解释
NSBundle *bundle = [NSBundle mainBundle]; //这里的作用就是获取应用程序的主目录,然后在主目录中寻找需要的资源

NSURL *plistURL = [boundle URLForResource:@"statedictionary" withExtension:@"plist"]; //通过主目录来获取资源的路径(URL),这里寻找的资源是statedictionary.plist,有了资源的路径,就可以找到该资源

NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfURL:plistURL]; //NSDictionary就是通过资源的路径来获取资源,并读取里面的数据

self.stateZips = dictionary; //将dictionary赋stateZips

NSArray *components = [self.stateZips allKeys]; //获取statedictionary.plist中所有的<key>

NSArray *sorted = [components sortedArrayUsingSelector:@selector(compare:)]; //将获取的所有<key>按字母序进行排列(这里我只知道是排序的意思,后面的方法我还没有深究,先这样子用着)

self.states = sorted; //将排完序的数组赋给states(左边的component)

NSString *selectedState = [self.states objectAtIndex:0]; // 当启动程序的时候,左边的component默认是选择第一个值的,这里获得第一个值,然后根据这个值找到对应的zips,然后显示在右边的component中

NSArray *array = [stateZips objectForKey:selectedState]; // 根据<key>来获得对应的zips

self.zips = array; //然后将数组赋给zips

继续写code

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.picker = nil;
    self.stateZips = nil;
    self.states = nil;
    self.zips = nil;
}

不多解释了

#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];
}

Picker的DataSource方法,第一个方法返回2,说明有2个component,第二个方法返回component中数据的个数,这里以component做为区分,如果是kStateComponent,返回左边component数据的格式,否则返回右边component数据的个数。

#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];
    }
}

Picker的delegate方法,第一个方法还是和以前一样的,第二个方法是重点,用来更新右边component的数据,更新的方法和viewDidLoad中的方法是一样的,首先判断是不是左边的component发生了变化,如果是,得到左边component的当前值,根据这个值获取相应的zips,然后将zips赋值给右边的component,接着选中右边component的第一个值,并以动画的形式选中,滚动到第一个值,最后重新载入右边的component。

好了,以编译运行程序了,程序运行后,选择第四个tabitem

试着改变左边component的选项,右边的zips会跟着改变

程序似乎是做完了,但是并不完美,稍微往下滚动左边的component,会发现一些state的名字太长,没有完全显示出来,但是又发现右边的component有很多空白的地方被浪费了,因此最后能够拉长左边的component,缩短右边的component,方法很简单,在BIDDependentComponentPickerViewController.m中添加下面的方法

- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
    if(component == kZipComponent)
        return 90;
    return 200;
}

该方法也是一个delegate,它用来设置pickerview中各个component的宽度,再次编译运行程序,这次程序界面完美了

点击button,一个警告框弹出,告知你选择的内容

20)Custom Picker
Picker View中component所包含的对象不仅仅是string,也可以是其他,比如图片,在这个例子中,我们将做一个简单的游戏,类似于赌博机中的摇连续的“777”,一共会有5个component,每个component中包含5张不同的图片,然后根据随机数进行滚动,如果恰巧有连续3个component选中的图片一样,则获得胜利。

打开BIDCustomPickerViewController.h文件,添加如下code

#import <UIKit/UIKit.h>

@interface BIDCustomPickerViewController : UIViewController <UIPickerViewDelegate, UIPickerViewDataSource>

@property (strong, nonatomic) IBOutlet UIPickerView *picker;
@property (strong, nonatomic) IBOutlet UILabel *winLabel;
@property (strong, nonatomic) NSArray *column1;
@property (strong, nonatomic) NSArray *column2;
@property (strong, nonatomic) NSArray *column3;
@property (strong, nonatomic) NSArray *column4;
@property (strong, nonatomic) NSArray *column5;

- (IBAction)spin;

@end

这里多声明了一个UILabel的outlet,当我们赢得一局后,label会显示“WIN”,否则label不显示,因此我们需要一个outlet来指向label。然后声明了5个NSArray,因为有5个component,所以需要5个NSArray与之对应。

打开BIDCustomPickerViewController.xib,拖一个Picker View放在view的顶部,选中Picker View,在Attributes inspector中找到Interaction,将User Interaction Enabled前的勾去掉,这样就不能用手指去滚动component了。

然后拖一个Label放在Picker View下面,将Label的边框拉长到左右两边出现辅助线的位置,并在Attributes inspector中将Label的字体大小设置为48,颜色设为你喜欢的颜色即可,Label文字居中,在选中Label的状态下,使用快捷键command + =使Label边框的大小根据Label字体的大小进行改动。再拖一个button放在Label下面,并命名为Spin。

接着就是连接outlet和action了,PickerView连接到picker outlet,Label连接到winLabel outlet,button连接到spin action,PickerView的dataSource和delegate连接到File's Owner。这些应该都很简单了,和之前的一样。

21)添加图片资源,继续写code
首先下载图片资源:CustomPickerImages,解压缩后拖入当Project navigator的Pickers目录下

里面一共有6张png图片

打开BIDCustomPickerViewController.m文件,添加如下代码 

#import "BIDCustomPickerViewController.h"

@implementation BIDCustomPickerViewController
@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 = @"";
}

就解释一下spin方法中的for循环中

    for (int i = 0; i < 5; i++) {

        int newValue = random() % [self.column1 count]; //通过产生随机数,来确定component滚动到哪个位置,要对产生的随机数进行取余操作,否则很容易就超出component对象的个数,导致内存溢出报错。这里的余数表示component滚动到第几个位置

        if(newValue == lastVal) //相等时,说明随机数的余数和前一次的余数相等

            numInRow++; //计数器加1

        else

            numInRow = 1; //计数器重新置1,从新开始计数,最开始的时候计数器赋的值是-1,它不可能和任何余数相等,第一个component也不可能和任何其他的component进行对比,因此第一次执行for循环总是会跳到这里

        lastVal = newValue; //记录最近一次的余数,用于下次比较

        [picker selectRow:newValue inComponent:i animated:YES]; //设置picker中当前的component滚动到哪个位置

        [picker reloadComponent:i]; //重新载入当前的component

        if (numInRow >= 3) { //如果numInRow的值大于等于3,说明有连续3个相同的图标出现了

            win = YES; //连续出现3个相同的图标,赢得本局

        }

    }

继续添加viewDidLoad和viewDidUnload

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    
    winLabel.text = @"";
    
    UIImage *seven = [UIImage imageNamed:@"seven.png"];
    UIImage *bar = [UIImage imageNamed:@"bar.png"];
    UIImage *crown = [UIImage imageNamed:@"crown.png"];
    UIImage *cherry = [UIImage imageNamed:@"cherry.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];
    }
    
    srandom(time(NULL));

}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.picker = nil;
    self.winLabel = nil;
    self.column1 = nil;
    self.column2 = nil;
    self.column3 = nil;
    self.column4 = nil;
    self.column5 = nil;
}

就解释一下viewDidLoad中某些语句(viewDidUnload很简单,就不做说明了):

UIImage *seven = [UIImage imageNamed:@"seven.png"]; //imageNamed方法是根据图片的名称去寻找图片并载入

UIImageView *sevenView = [[UIImageView alloc] initWithImage:seven]; //在for循环中根据UIImage对象创建6个UIImageView

NSArray *imageViewArray = [[NSArray alloc] initWithObjects:sevenView, barView, crownView, cherryView, lemonView, appleView, nil]; //创建一个Array,大家应该可以猜到,这个数组会赋给component,NSArray保存的对象是object,因此不论是string还是UIImageView,都可以保持在里面

NSString *fieldName = [[NSString alloc] initWithFormat:@"column%d", i]; //之前声明了5个NSArray,它们的名字都是有规律的column*

[self setValue:imageViewArray forKey:fieldName]; //setValue:forKey:是根据属性(property)的名字来为其赋值,我们使用这个方法,来为5个column*赋值

srandom(time(NULL)); //设置随机数的因子,可以是每次产生的随机数不同

最后添加dataSource和delegate方法

#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];
}

DataSource方法和之前的并无两样,就不解释了,而delegate方法则稍稍有些不同,之前返回的董事NSString,这里返回的UIView,因为component中的对象是UIImageView

NSString *arrayName = [[NSString alloc] initWithFormat:@"column%d", component + 1]; //参数component传过来的值是0~4,所以必须要加1,才能对应到column1~column5

NSArray *array = [self valueForKey:arrayName]; //valueForKey根据property的名字得到对象

return [array objectAtIndex:row]; //根据得到的array返回第row个对象

22)编译运行
选择第五个tabitem,程序的初始界面

点击多次Spin后,终于获胜

23)总结
这篇文章主要讲解了TabBar和PickerView的使用,TabBar作为root controller,控制了5个subview的切换,PickerView有2种,一种是DatePicker,另一种是PickerView,DatePicker专门处理日期,比较简单。PickerView相对复杂一些,需要引入<UIPickerViewDelegate, UIPickerViewDataSource>,并使用其中的方法告知PickerView中有多少个component和每个component中有多少个对象,还要绑定NSArray,并自己写返回对象的方法。

24)一个小问题
如果你和我使用的是同一个版本的xcode

那在编译项目的时候,始终会有一个感叹号存在

TabBarController.xib: warning: Attribute Unavailable: Defines Presentation Context is not available prior to Xcode 4.2.

我也不太清楚是什么意思,但是查了一下网,选中TabBarController.xib,在Attributes inspector中,把Defines Context前的勾去掉就可以,然后编译就没有感叹号了。

 

Pickers


 

 

 

 

 

 

 

 

posted @ 2012-12-27 23:23 minglz 阅读(...) 评论(...) 编辑 收藏