在前面几部分中,主屏幕只能展示一个汽车对象的信息。而在实际iOS中,一次显示多条数据并实现滚动查看是十分常见的,例如通讯录、音乐以及其他多个以表格样式展示数据的应用程序。
表视图是开发者的工具包中至关重要的控件之一,当设计和创建那些运行在屏幕空间不大的iPhone/iPad设备上的应用陈旭时,该视图的作用尤为重要。与UINavigationController结合使用时,表视图控件能建议地展示有着层次结构的数据,轻松实现导航。表视图允许用户便捷地访问对象概要列表。本节将介绍有关表视图的一些基本知识。
首先我们先创建一个Empty Application的模板(当然xcode6以后就没有这个了),但是没关系,我们正常创建一个 Single View Application模板,然后将 ViewController.m、ViewController.h和Main.storyboard删除。
(方法查自于http://www.cocoachina.com/bbs/read.php?tid=231701)
这样就创建了一个Empty Application了。
接下来在xcode中选择file|new,添加一个iphone故事板文件,选中user interface类别中的storyboard,添加到文件中,将.plist将Main storyboard file base name 那项删掉中的Main改成刚才添加的那个故事面版文件。
我们得到了一个什么都没有的故事面板文件,这时候我们将一个表视图控制器(Table View Controller)拖入到故事面板文件中,然后点击刚才放入的故事面板,将 Is Initial View Controller 选中
好了,创建完了
现在,最简单的诗经就是创建一些预设的单元格了
运行后,虽然看起来没变,但是点击上面三行表格时,表格会变成灰色表示选中,而之前是不行的
有时候我们想要创建预先定义的单元格。但更常见的情况是,我们想要显示数量不定、数量可变的数据项列表,因此我们就需要提前知道其内容。上面我们创建的表视图是使用静态单元格。在表格中,它们的表现类似于静态视图。但有时候,我们不知道需要显示多少单元格,以及如何来显示它们。此时,我们就需要使用动态原型。
当然,使用什么类型的单元格取决于我们要做什么,像setting显然我们是知道需要多少单元格的就使用静态单元格,而类似于备忘录的,则是使用动态原型。
选中Table View Cell,将Identifiler设置为MyCell
如果没有设置为MyCell(其他名字也行,总之要有名字),会有如下警告
这是在告诉我们:“原型单元格(prototype cell)没有设置重用标识符。重用标识符(reuse identifier)能让表视图控制器判定创建和重用哪一种类型的单元格原型
现在我们需要编写代码告诉表视图需要创建多少单元格,并需要给这些单元格填充数据,因此,我们需要一个UITableViewDataSource。
创建步骤:(跟以前创建的差不多,不过有点变化)
返回故事版,选中表视图控制器(再次说一下就是Table View Controller),并且使用Identity检查器将该类设置为MyTableViewController
在MyTableViewController.m中会有警告,就是文件中有#warning,这个特殊的标记是要告诉xcode有些事需要处理。警告是要告诉我们还需要写一写代码。
每个警告都告诉我们一些知识,有关表视图如何组织数据的方式。表视图的数据组(表格段)可以为0个或多个,每个表格段可以拥有0行或者多行的数据。个数从0开始计,看上去很奇怪,但是在表视图中,全控的数据集或仅仅是空的表格段都是有可能的
下面我们通过代码告诉表视图数据段有多少,以及每段数据含有多少行(其实从方法名称上就能大致知道怎么用了)
//这个方法是告诉表视图一共有多少数据段(该方法是可选的,如果没有提供该方法,则默认1)
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
//warning Incomplete implementation, return the number of sections
return 1;
}
//这个方法说明指定的数据段含有多少个数据项,这里我们弄5个
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//warning Incomplete implementation, return the number of rows
return 5;
}
在刚替换的两个方法下面的tableView:cellForRowAtIndexPath:方法中,将CellIndentifier的值由@"Cell"
改为@"MyCell
。这告诉表视图创建的新的单元格时应用哪个原型。
再次运行,这时候会有5个可以单击的单元格
首先,我们需要知道表视图中的Index Path是什么。表视图数据显示为一个或多个组。每个行组被称为section(表格段)。表中的任何行可以根据它的组和行的数字,进行识别、予以区分。这个组合被称为index Path(索引路径),(我觉得可以简单的认为是一个动态的二维数组)
在表视图中,所以路径使用的是NSIndexPath类中的一个更通用、专门针对表视图的版本来表示索引路径。它由两个整数组成:第一个是表格段编号,第二个是行号。因为索引路径是对象,所以可以发送消息或使用点表示法来访问其部件。表视图使用一组特殊访问器,section和row让代码更容易读且更可维护
如果在表中有个NSIndexPath名为cellIndexPath,那么可以使用cellIndexPath.section来获取它的表格段标号,用cellIndexPath.row来访问他的行。
代码如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell" forIndexPath:indexPath];
NSString *myLabel = [NSString stringWithFormat:@"@Section: %ld Row: %ld",(long)indexPath.section,(long)indexPath.row];
cell.textLabel.text = myLabel;
return cell;
}
运行结果:
现在我们要像表格中添加表格段。要实现这一点,仅仅需要将numberOfSectionsInTableView:
的返回值从1改为2,运行这个程序,则会有两个含有5分元素的表格段,但是它们没有表格段的表头视图。
添加表格段的表头视图,只需要在numberOfSectionsInTableView:
方法的下方插入以下代码
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [NSString stringWithFormat:@"Section: %ld",(long)section];
}
效果如下图
可以试着添加更多的表格段或再每个表格段中添加更多的单元格。即使添加的数量非常大,表视图还是能工作。部分原因是,表视图仅仅会保留那些处于可见的单元格,以及一些额外的单元格行,从而实现快速的滚动。如果创建一个含有数千数据的表格,内存中一次只保留几十个数据。表视图创建他所需要的单元格,并回收已经不在屏幕上的单元格。这正是重用标识符的用处。
PS可以到ArnoldYUV ,我的git上下载代码
首先,将Add/View场景替换为一个基于表视图控制器的场景:
#import
@class Car;
@interface CarTableViewCell : UITableViewCell
@property (strong, nonatomic) Car *myCar;
@end
#import "CarTableViewController.h"
#import "Car.h"
#import "CarTableVIewCell.h"
@interface CarTableViewController ()
@end
@implementation CarTableViewController{
NSMutableArray *arrayOfCars;//这是用来存车的数组
}
- (void)viewDidLoad {
[super viewDidLoad];
arrayOfCars = [NSMutableArray new];
[self newCar:nil];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
//根据有多少量车就设置多少单元格
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [arrayOfCars count];
}
tableView:cellForRowAtIndexPath:
中,确保静态的CellIdentifier正确设置。代码如下- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"CarCell";
//.. .. ..我忘了之前啥样子的.. .. .. 关键是要加上面这一句
//暂时这些 还有
}
运行程序后。表视图控制器现在是Add/View场景。在继续进行之前,将导航栏的标题设置为CarValet,也可以从ViewController.m中复制本地化代码
我们现在已经有了一辆汽车的数据了,以及一个现实这辆汽车的单元格。但是我们没有拥有视图元素来显示该数据。下一步就是修改原型单元格,从而显示汽车数据。步骤如下:
//在Car.h中
@property NSDate* dateCreated;//新加
//在Car.m中
- (id)init {
self = [super init];
if (self != nil) {
_year = kModelTYear;
_fuleAmount = 0.0f;
_dateCreated = [NSDate date];//新加
}
return self;
}
- (id)initWithMake:(NSString *)make
model:(NSString *)model
year:(int)year
fuelAmount:(float)fuelAmount {
self = [super init];
if (self !=nil) {
_make = [make copy];
_model = [model copy];
_year = year;
_fuleAmount = fuelAmount;
_dateCreated = [NSDate date];//新加
}
return self;
}
在故事板上,打开汽车表视图的单元格的Attributes检查器,并且将其设置为RightDetail样式(其实上面已经有图这么弄了),这是表格的样式会改变,会在坐标以黑色显示标题,并且在右边以浅灰色显示详情。将详情的字号设置为12。
向CarTableViewCell中添加一个公有方法,代码如下,注意要在.h文件中声明
//.h文件 增加
- (void)configureCell;
//.m文件
- (void)configureCell {
NSString *make = (self.myCar.make == nil) ? @"Unknown" : self.myCar.make;//1 汽车的make或model可能为nil,所以将其设置为默认值
NSString *model = (self.myCar.model == nil) ? @"Unknown" : self.myCar.model;
self.textLabel.text = [NSString stringWithFormat:@"%d %@ %@",self.myCar.year,make,model];//2 将主编前设置为汽车的year、make和model
NSString *dateStr = [NSDateFormatter localizedStringFromDate:self.myCar.dateCreated
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterShortStyle];//3 获得本地化版本的创建日期,日期样式应尽可能简短
NSLog(@"%@", self.myCar.dateCreated);
self.detailTextLabel.text = dateStr;//4 将段版本的创建日期设置到详细信息文本去中
}
//上面说少一部分的那段代码完整的在这里
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"CarCell";
CarTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.myCar = arrayOfCars[indexPath.row];
[cell configureCell];
return cell;
}
//注意必须修改我们所创建的单元格的类,因为只有这样操作,它才能知道myCar属性和configureCell方法
我们已经有了一个添加汽车的方法(就是表示图文件那个newcar的方法)。我们只需要通过一个按钮就可以实现对他的调用了。
在newCar:方法的末尾添加如下代码让表视图重载自己(不然你点击后会发现程序貌似没有变,其实已经变了,只是没有刷新)
[self.tableView reloadData];
如果能让新车带动画的进入表格顶部,应该会更好一点。对newCar方法进行修改,代码如下:
- (IBAction)newCar:(id)sender {
Car *newCar = [Car new];
[arrayOfCars insertObject:newCar atIndex:0];//1 将汽车插入到数组的前边
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];//2 创建一个NSindexPath对象来指定新单元格的位置——他的section和row
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];//3 让表视图在新的索引路径上插入一个对象。这回事表视图调用数据源来查找第0表格段第0行的数据——也就是数组的第一个元素。因为数组已经被更新,所以新的单元格会被返回
//[self.tableView reloadData];
}
(运行如图:PS看前面增加的效果就行了……后面的是后续的内容)
能够创建汽车,当然也就可以删除汽车了。其实,在我们添加CarTableViewController类的时候,一个简单的删除方法所需要的大部分代码已经创建好了。我们只需要将他的注释删除掉就可以了
在CarTableViewController.m修改代码如下:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return YES;//这里如果返回的是no那么就是不能编辑的包括删除,假设我们写的是return (indexPath.row%2!=0);那么0,2,4这些行数就不能进行操作了,下面给出一个不能删除的模拟运行图
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
[arrayOfCars removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
} //else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
//}
}
(这是之前截的图不要在意内容,在意要展示的效果就行啦)
扫动是很好的,但是如果是新手的话,可能不知道扫动这个手势。所以我们最好是提供一个用户界面元素用于编辑会好一些。为此,通过以下步骤添加一个编辑按钮:
打开故事面板。病将两个bar button item拖动视图元素列表中First Responder下面的表视图控制器浏览器中
为刚才添加的两个工具栏按钮一个命名为Edit,另一个命名为Done
self.navigationItem.leftBarButtonItem = self.editButton;//为导航栏添加左按钮
- (IBAction)editTableView:(id)sender {
BOOL startEdit = (sender == self.editButton); //1 如果发送这条消息的是编辑按钮,那么开始编辑
UIBarButtonItem *nextButton = (startEdit) ? self.doneButton :self.editButton; //2 下一个要显示的按钮是当前没有显示的那个,就是交替出现done和edit
[self.navigationItem setLeftBarButtonItem:nextButton animated:YES];//3为新的导航栏按钮添加动画
[self.tableView setEditing:startEdit animated:YES];//4让表视图动画过度到编辑或非编辑状态
}
运行如图:
在本节中,我们将添加一些场景用来查看和编辑汽车。
在故事版中完成创建汽车详情屏幕的大多数可视化工作:
选择表视图并将内容设置为static cells,将样式设置为Grouped。当修改样式时,会看到表格段数目的设置。将表格设置为三个表格段
基于每个组要显示的数据数目来修改其中的单元格数目:
选中第一个分组,然后Attributes检查器会发生变化,允许修改行数、表头和表尾。将行数保持为3,将表头设置为Make、Model&year。这些表头文字要添加到故事板的分组中
将带有一个单元格的第二个分组的表头设置为Fuel
将第三个分组的表头设置为Date PARKED
对每个单元格,将类型设置为Basic,将内容设置到它所显示的数据元素的名称。例如,第一个分组的三个单元格分别为Make,Model,Year
最后的结果图如下
现在看看这个视图:
结果图如下:
运行结果如下:
要为查看汽车场景填入数据,需要为这个场景的视图控制器制定一个自定义类,步骤如下:
@class Car;
@property Car *myCar
#import
@class Car;
@interface ViewCarTableViewController : UITableViewController
@property (weak, nonatomic) IBOutlet UILabel *makeLabel;
@property (weak, nonatomic) IBOutlet UILabel *modelLabel;
@property (weak, nonatomic) IBOutlet UILabel *yearLabel;
@property (weak, nonatomic) IBOutlet UILabel *fuelLabel;
@property (weak, nonatomic) IBOutlet UILabel *dateLabel;
@property (weak, nonatomic) IBOutlet UILabel *timeLabel;
@property Car *myCar;
@end
- (void)viewDidLoad {
[super viewDidLoad];
dataUpdated = NO;
self.myCar = [self.delegate carToView];
self.makeLabel.text = (self.myCar.make == nil) ? @"Unknown" : self.myCar.make;
self.modelLabel.text = (self.myCar.model == nil) ? @"Unknown" : self.myCar.model;
self.yearLabel.text = [NSString stringWithFormat:@"%d",self.myCar.year];
self.fuelLabel.text = [NSString stringWithFormat:@"%0.2f",self.myCar.fuelAmount];
self.dateLabel.text = [NSDateFormatter localizedStringFromDate:self.myCar.dateCreated
dateStyle:NSDateFormatterMediumStyle
timeStyle:NSDateFormatterNoStyle];
self.timeLabel.text = [NSDateFormatter localizedStringFromDate:self.myCar.dateCreated
dateStyle:NSDateFormatterNoStyle
timeStyle:NSDateFormatterMediumStyle];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"ViewSegue"]) {
ViewCarTableViewController *nextController;
nextController = segue.destinationViewController;
// NSInteger index = [self.tableView indexPathForSelectedRow].row;
// nextController.myCar = arrayOfCars[index];
nextController.delegate = self;
}
}
为ViewController类创建类似的prepareForSeguesender:方法。在此处,为传进来的ViewCarTableViewController设置Car对象
运行程序,穿件一些汽车,并单机其中的一些。新的查看汽车场景会打开,我们能够多动这个表格,并且旋转也能正确处理。实际上,我们创建的新场景都能处理旋转,改变他们的单元格尺寸,并根据需要来支持滚动
选中一辆不同的汽车时,这个程序显示含有正确数据的新增加汽车视图屏幕、当然这些详情屏幕的去呗只有Date Packed文本区。现在该添加make、model和year的编辑控件了
对了,两个表视图控制器的底部可能会有一个白条(忘了前面有没有取消掉了)。这是之前章节创建的工具栏。可以自己尝试的去删除他。
汽车的大部分数据都可编辑。我们需要提供场景来编辑汽车的不同属性。首先是make和model(这俩一样)。所有元素都基于文本,因此他们都可以使用同一类场景,步骤如下:
新的控制器用于编辑汽车的品牌或型号。这意味着,在视图显示之前,标题、标签和文本框数据需要被设置好。有两种方法实现:查看汽车场景要么可在prepareForSeguesender:里面设置这些属性,要么可以使用协议。使用协议是更好的方法,因为具有更高的灵活性和可维护性,另外还意味着可以在其他项目中重用这些元素
到此为止,当改变当前屏幕时,可以沿着视图层次向上或乡下导航,或者切换到一个新的标签页或视图集合。在一个层次结构中,我们从所有汽车的概览,到达一辆汽车的详情。随着层次深入,用户可以看到视图从右侧滑入并且当向上返回时,从左侧消失
有事我们想要显示某些视图层次结构之外的东西。编辑汽车的特定属性是个示例。编辑并不是修改某件东西的能力;我们还提供取消修改的机会,即使用户已经输入了新的值
这样的操作是不同的,这是向用户表明他或她并不只向前进一步操作的好方式。使用一个模态屏幕实现该要求,这能迫使用户做出选择。用户做出修改后既可以接受也可以取消。用户现在不能浏览其他地方的应用程序,直到做出选择之后。模态屏幕,设置切换的视觉效果也与通常不同。模态屏幕中默认切换效果是从底部和下面滑入,结束时消失
在iOS中,可以将任何场景(或视图控制器)显示为模式。如何实现转场有不同选项。正如下面我们将看到的,可以使用模态segue或对当前导航控制器的不同调用
而当用户已经完成编辑时,触摸Done或Cancel的一个,回到原来的显示模态屏幕的场景。当使用segue显示模态屏幕时,返回叫回退(unwinding)
步骤如下,创建交换数据的协议:
#import
@protocol MakeModelEditProtocol <NSObject>
- (NSString*)titleText;
- (NSString*)editLabelText;
- (NSString*)editFieldText;
- (NSString*)editFieldPlaceholderText;
- (void)editDone:(NSString*)textFieldValue;
@end
#import "MakeModelEditProtocol.h"
@property (weak, nonatomic) id delegate;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.myNavigationItem.title = [self.delegate titleText];
self.editLabel.text = [self.delegate editLabelText];
self.editField.text = [self.delegate editFieldText];
self.editField.placeholder = [self.delegate editFieldPlaceholderText];
[self.editField becomeFirstResponder];
}
- (IBAction)editCancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)editDone:(id)sender {
[self.delegate editDone:self.editField.text];
[self dismissViewControllerAnimated:YES completion:nil];
}
<MakeModelEditProtocol>
上面代码表示ViewCarTableViewController同意遵照该协议
- (NSString*)titleText {
}
- (NSString*)editLabelText {
}
- (NSString*)editFieldText {
}
- (NSString*)editFieldPlaceholderText {
}
make/model编辑器已经能够编辑一些内容并让委托得知结果。下一步就是准备委托,在当前这种情况下,也就是VIewCarTableViewController。这个不寻常的部分用于跟踪当前正在被编辑的是哪个数据字段
make/model编辑器根本不在乎编辑的是什么。他可以编辑任何文本元素。所有上下文都是由delegate设置的,这意味着查看汽车控制器需要跟踪哪个输入框正被编辑
通过下面步骤完成添加汽车品牌和型号的编辑工能:
#define kCurrentEditMake 0
#define kCurrentEditModel 1
@interface ViewCarTableViewController () {
NSInteger currentEdutType;
}
在故事板中,添加从品牌单元格到make/model编辑场景的模态选择segue(不是辅助动作)。选中这个色鬼并且将标识符设置为MakeEditSegue。当设置标识符时,确保风格为Model。使用型号单元格重复这个动作,并且将标识符设置为ModelEditSegue
使用prepareForSeguesender:创建Make/Model编辑控制器。使用该方法设置正在编辑的是哪个数据字段。记得导入编辑器的头文件,在当前这种情况下,是MakeModelEditViewController.h。在ViewCarTableViewController.m
中,代码如下:
#import "MakeModelEditViewController.h"
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"MakeEditSegue"]) {
MakeModelEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
currentEdutType = kCurrentEditMake;
} else if ([segue.identifier isEqualToString:@"ModelEditSegue"]) {
MakeModelEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
currentEdutType = kCurrentEditModel;
}
}
- (NSString*)titleText {
NSString *titleString = @"";
switch (currentEdutType) {
case kCurrentEditMake:
titleString = @"Make";
break;
case kCurrentEditModel:
titleString = @"Model";
break;
default:
break;
}
return titleString;
}
- (NSString*)editLabelText {
NSString *titleString = @"";
switch (currentEdutType) {
case kCurrentEditMake:
titleString = @"Enter the Make:";
break;
case kCurrentEditModel:
titleString = @"Enter the Model:";
break;
default:
break;
}
return titleString;
}
- (NSString*)editFieldText {
NSString *titleString = @"";
switch (currentEdutType) {
case kCurrentEditMake:
titleString = self.myCar.make;
break;
case kCurrentEditModel:
titleString = self.myCar.model;
break;
default:
break;
}
return titleString;
}
- (NSString*)editFieldPlaceholderText {
NSString *titleString = @"";
switch (currentEdutType) {
case kCurrentEditMake:
titleString = @"Car Make";
break;
case kCurrentEditModel:
titleString = @"Car Model";
break;
default:
break;
}
return titleString;
}
- (void)editDone:(NSString*)textFieldValue {
if(textFieldValue != nil && [textFieldValue length] > 0) { //1 仅当有新的文本时才更新。如果文本框一开始根据editFieldText设置为nil,而且现在仍然是nil,那么不做任何改变。它也可以是一个空字符串
switch (currentEdutType) {
case kCurrentEditMake:
if (self.myCar.make == nil || !([self.myCar.make isEqualToString:textFieldValue])) { //2 文本框中有一些文字,如果当前没有值或者新值与旧值不同,则更新
self.myCar.make = textFieldValue; //3 如果有更新,则修改Car对象
self.makeLabel.text = textFieldValue; // 4 在汽车视图中修改标签
dataUpdated = YES;
}
break;
case kCurrentEditModel:
if (self.myCar.model == nil || !([self.myCar.model isEqualToString:textFieldValue])) {
self.myCar.model = textFieldValue;
self.modelLabel.text = textFieldValue;
dataUpdated = YES;
}
default:
break;
}
}
}
运行结果如下:
我们发现在根目录中并没有更新,这就是下面要解决的问题
不存在从查看汽车控制器连接回汽车表格控制器的东西。唯一的通信是在CarTableViewController为进入的汽车视图设置myCar时,发生在prepareForSegue:sender:方法中。我们需要一个协议来让两个视图控制器能够通信
这个协议需要两条消息:一条用于设置要查看哪辆汽车,还有一条用于在数据发生变化时通知委托对象。最后一部分意味着查看汽车控制器必须跟踪是否有变化。
#import
@class Car;
@protocol ViewCarProtocol <NSObject>
- (Car*)carToView;
- (void)carViewDone: (BOOL)dataChanged;
@end
@property (weak, nonatomic) id delegate;
self.myCar = [self.delegate carToView];
<ViewCarProtocol>
- (Car *)carToView {
NSInteger index = [self.tableView indexPathForSelectedRow].row;
return arrayOfCars[index];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"ViewSegue"]) {
ViewCarTableViewController *nextController;
nextController = segue.destinationViewController;
// NSInteger index = [self.tableView indexPathForSelectedRow].row;
// nextController.myCar = arrayOfCars[index];
nextController.delegate = self;
}
}
下图显示了当前的转移行为以及他们如何发生。这里有三个segue:从汽车列表到查看汽车的push类型,还有从查看汽车场景到make/model编辑场景的两个model类型。最后还有才能够make/model编辑场景回退到查看汽车场景,方法是使用dismissControllerAnimated:completion:
从查看汽车场景到汽车表格场景的转换时个隐式的unwind segue,源自一个并未控制的用户界面元素。用户单机导航栏中的Back按钮,从而segue展开,但并未调用故事版和segue。
然而,存在另一个机制,即Back按钮由导航控制器进行管理。每当视图控制器被推入(pushed)或从堆栈中弹出(popped off)时,导航控制器就会查找可选的委托,并能在这次转场的前后都发送消息
为了捕捉从汽车查看场景返回到汽车表格场景的转换,需要将正确的控制器设置为导航控制器的委托,然后实现合适的方法。当前协议位于查看汽车表视图的中间,因此这就是需要监听场景切换的地方:
<MakeModelEditProtocol, UINavigationControllerDelegate>
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.navigationController.delegate = self;
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (viewController == (UIViewController*)self.delegate) {
if (dataUpdated) {
[self.delegate carViewDone:dataUpdated];
}
navigationController.delegate = nil;
}
}
- (void)carViewDone:(BOOL)dataChanged {
if (dataChanged) {
[self.tableView reloadData];
}
}
运行结果如下:
当使用carViewDone:方法中的reloadTable是我,我们告诉表视图,每个单元格中的内容是无效的,所有的内容都是过期的。这会导致表视图至少会对所有的(通常会更多)展示给用户的单元格进行更新。在应用程序中,这非常快,因为数据简单。但是当数据复杂又多的时候,这可能就变得很慢了
所以,我们应该只告诉表格更新那些发生改变的单元格
reloadRowsAtIndexPath:withRowAnimation:方法更新特定集合中的单元格。要使用这个方法,必须知道被查看单元格的索引路径——也即是被选中的单元格。所有我们能够获得的被选中的单元格的索引路径,可以通过使用indexPathForSelectedRow方法得到。步骤如下:
@implementation CarTableViewController{
NSMutableArray *arrayOfCars;
NSIndexPath *currentViewCarPath;//添加的
}
- (Car *)carToView {
// NSInteger index = [self.tableView indexPathForSelectedRow].row;
currentViewCarPath = [self.tableView indexPathForSelectedRow];
// return arrayOfCars[index];
return arrayOfCars[currentViewCarPath.row];
}
- (void)carViewDone:(BOOL)dataChanged {
// if (dataChanged) {
// [self.tableView reloadData];
// }
if (dataChanged) {
[self.tableView reloadRowsAtIndexPaths:@[currentViewCarPath] withRowAnimation:YES];
}
currentViewCarPath = nil;
}
型号和品牌都基于文本。可以使用文本标示年份并且展示数字文本的键盘。但这并不能确保用户的输入在特定范围年数内的4位年份数。相反,可以创建一个基于选择器的编辑视图,特定于罗马时间
UIPickerView控件可以让我们展示更长的、由单个选项甚至多个分栏组成的列表。闹铃就是一个例子
选择器看似是老虎机,由回转轴和回转轴上的多个位置组成。可指定回转轴上的数字和回转轴上每个数字的位置。可以通过实现UIPickerViewDataSource协议里的方法来设置回转轴
也需要设置每个位置的内容、这一点如同字符串一样简单,也可同特定宽度和高度的自定义视图一样复杂。可以在UIPickerDelegateProtocol协议的方法中进行设置。委托也会在行被选中时通知我们,这点对于动态的更新选项或者其他部分的用户界面都是有用的
不像老虎机,我们只需要回转轴。需要将选择器视图放置到视图控制器中作为年份编辑器,步骤如下:
为Cancel和Done分别添加action,名称分别为editCancel和editDone
所以这时候YearEditViewController.h应该有如下代码:
- (IBAction)editCancel:(id)sender;
- (IBAction)editDone:(id)sender;
@property (weak, nonatomic) IBOutlet UIPickerView *editPicker;
一辆汽车的品牌年份是个有范围的数字:不会大于当前年份,也不会小于第一辆生产的汽车
在修改年份编辑器之前,更新Car对象,使用新的最早的品牌年份:
#define kModelTYear 1908
步骤如下:
<UIPickerViewDelegate,UIPickerViewDataSource>
```
- (NSInteger)getYearFromDate:(NSData*)theDate {
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];//1 年份要求将日期分解为组件,这需要有日历。该方法只针对罗马日了,虽然可以使用当前系统日历来对返回的值进行本地化
NSDateComponents *components;
components = [gregorian components:NSYearCalendarUnit fromDate:theDate];//2 从theDate返回日期的一个组件对象,用于初始化年份
return components.year; //3 返回日期组件
}
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
NSInteger maxYear = [self getYearFromDate:[NSDate date]];
maxYear += 1;
return (maxYear - kModelTYear);
}
- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component {
NSInteger totalRows = [pickerView numberOfRowsInComponent:component];
NSInteger displayVal = ((kModelTYear + totalRows) - 1) -row;
return [NSString stringWithFormat:@"%d", displayVal];
}
使用故事板将dataSource和delegate的旋转指针的outlet于编辑控制器的年份连接起来
在开始测试之前,需要创建一个segue以打开年份编辑器:
将一个展开的附件指示器(disclose accessory indicator)添加到年份单元格
协议非常简单,年份编辑器需要年份用展示,委托需要知道在按钮Done被选中时所选择的年份
添加委托需要两个关键的公式。第一个公式设置初始选择器的行,用于当前的年份。最大的年份选中最顶上的那条,索引为0。最初的想法可能是选中目标年份,然后减去最小的年份数以获得那行。然而,这并不适用于:年份越大,差异越大,也就是索引数。我们要让最大年份数减去目标年份:Row=MaximumYear-TargetYear
第二个公式将选中的行转为年份。再次,这是个相反的问题。索引为0的那行是最大年份:索引越大,年份越小。在此,年份是最大年份减去当前的行数:SelectedYear=MaximumYear-Row
这仅仅得到的是获得的最大年份,并且是单纯的总行数加上最小年份数后减一:MaximumYear=(MinimumYear+TotalRows)-1
最后一种选择是如何为选择器展示默认的选中的年份,如果没有设置值,即汽车对象将年份设置为1908。既然大多数人驾驶的汽车为当前年份前后左右的那些年份,最后的选择是展示这一点。可以展示默认年份为1908,但是驾驶1908年汽车的人概率太小了
遵循下列步骤以创建协议和相关的属性与方法:
#import
@protocol YearEditProtocol <NSObject>
- (NSInteger) editYearValue;
- (void) editYearDone:(NSInteger)yearValue;
@end
@property (weak, nonatomic) id delegate;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view
NSInteger yearValue = [self.delegate editYearValue];
if (yearValue == kModelTYear) {
yearValue = [self getYearFromDate:[NSDate date]];
}
NSInteger rows = [self.editPicker numberOfRowsInComponent:0];
NSInteger maxYear = (kModelTYear + rows) - 1;
NSInteger row = maxYear -yearValue;
[self.editPicker selectRow:row inComponent:0 animated:YES];
}
- (IBAction)editCancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)editDone:(id)sender {
NSInteger rows = [self.editPicker numberOfRowsInComponent:0];
NSInteger maxYear = (kModelTYear + rows) -1;
NSInteger year = maxYear - [self.editPicker selectedRowInComponent:0];
[self.delegate editYearDone:year];
[self dismissViewControllerAnimated:YES completion:nil];
}
@end
<MakeModelEditProtocol, UINavigationControllerDelegate,YearEditProtocol>
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"MakeEditSegue"]) {
MakeModelEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
currentEdutType = kCurrentEditMake;
} else if ([segue.identifier isEqualToString:@"ModelEditSegue"]) {
MakeModelEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
currentEdutType = kCurrentEditModel;
} else if ([segue.identifier isEqualToString:@"YearEditSegue"]) {//下面是新加的
YearEditViewController *nextController;
nextController = segue.destinationViewController;
nextController.delegate = self;
}
}
- (NSInteger)editYearValue {
return self.myCar.year;
}
- (void)editYearDone:(NSInteger)yearValue {
if(yearValue != self.myCar.year) {
self.myCar.year = yearValue;
self.yearLabel.text = [NSString stringWithFormat:@"%d",self.myCar.year];
dataUpdated = YES;
}
最后的运行结果如图:
今天就介绍到这里咯
可以到ArnoldYU ,我的git上下载代码
我的另一个博客站点:Arnold-你们好啊