在展示TableView前要配置好, 设置TableView的delegate和DataSource对象.
本章用到的代码来之demo:UITableView Fundamentals for iOS和TheElements
创建TableView的基本知识
为创建TableView, 需要创建几个相互交互的实体对象:视图控制器, TableView自己和tableView的DataSource和delegate对象, 视图控制器, DataSource和delegate通常是同一个对象. 视图控制器的启动了调用序列, 图3-1使用展示了这一过程.
- 视图控制器使用frame和style来创建tableView实例. frame通常为screen的frame,减去各种bar(导航/状态栏等)的高度即可. 在此刻也可设置一个全局变量表保存tableView, 或者用一个属性保存tableView的全局属性, 比如, 行高, autoresizing行为.
- 视图控制器给tableView设置DataSource和delegate, 然后给TableView发送
reloadData
消息. - TableView会给DataSource发送
numberOfSectionsInTableView:
消息, DataSource会使用该方法返回TableView的section数, 这个方法时可选的, 但如果你的tableView中有多个section的话, 必须实现该方法来告知TableView具有多个section. - 对于每一个section, DataSource都会收到TableView发送来的
TableView:numberOfRowsInSection:
消息, 该方法中返回每个section中的行数 - 之后DataSource会收到对应的可见cell的
tableView:cellForRowAtIndexPath:
消息, 然后返回对应cell的UITableViewCell
对象, tableView使用该对象去绘制表视图中的行.
图4-1展示了DataSource中的required方法加上numberOfSectionsInTableView:
. DataSource和delegate除了实现required方法外, 可以实现其他optional方法来提供一些额外的特性, 比如实现tableView:titleForHeaderInSection:
方法来给section提供title. 你可以创建plain或者grouped样式的tableView, 尽管以两种样式创建表视图的过程是相同的, 但是您可能希望执行不同类型的配置. 例如, 因为分组表视图通常呈现项目细节, 所以您可能还希望添加自定义附件视图(例如, 开关和滑块)或自定义内容(例如, 文本字段). 具体见详细地看看tableView中的cell
创建和配置TableView的一些建议
在APP中, tableView的用法很多, 想怎么用就怎么用, 你可以自定义一个对象去创建, 管理, 配置tableView, 但是你应该使用UIKit提供的设计好的一些关于tableView的类和技术, 也应该遵循UIKit对于tableView使用的一些规范, 比如数据/表现分离, 下面是UIKit关于创建和使用tableView的一些建议:
- 尽量使用
UITableViewController
来创建和管理TableView - 如果你的APP中大量使用TableView的话, 那么你在创建xcode工程时, 应该使用Master-Detail应用程序模板
- 对于展示连续的TableView, 你应该使用自定义的
UITableViewController
, 这样你既可以从storyboard中加载TableView, 也可以通过代码创建一个关联TableView.
如果你的界面是多个视图组合而成, 而TableView只是组成界面的一部分, 那么你应该使用UIViewController
来管理tableView, 不能用UITableViewController
, 因为后者会使得tableView的size固定填充屏幕(减去各种bar的空间).
使用storyboard来创建TableView
使用xcode创建包含tableView的APP时, 选则模板应该包含方便创建TableView的代码存根和storyboard, 该模板提供一个骨架, 你只需添加而外的代码和设置即可, 这样很方便
创建基于TableView的APP:
- 选择Xcode, 选择文件>新建>项目
- 在弹出的对话框的左侧,选择iOS, 选择应用程序
- 在对话框的中间主要区域中, 选择'Mater-Detail 应用程序', 点击'下一步'
- 选择项目选项(确保使用storyboard), 然后点击'下一步'
- 选择项目的保存位置, 然后点击'创建'
在第四步选择设备时, 会决定项目中的storyboard数量. 点击项目导航栏中的storyboard文件, 会显示storyboard编辑视图. 如果你选择的是iPhone, 那么storyboard中包含一个TableViewController, 如图3-2所示.
确保画布上的场景表示代码中的主视图控制器
- 在画布上, 单击场景标题来选择表视图控制器
- 单击工具栏区域顶部的
Identity button
来打开Identity inspector - 检查工程中的类是否包含UITableViewController的子类
选择TableView的Display Style
TableView风格包括两种:plain和grouped
在storyboard中选择TableView的style
- 点击scene的中间来选中tableView.
- 在工具栏中选择Attributes inspector
- 在tableView的Attributes inspector中选择plain或者grouped style
选择TableView的Content Type
在设计tableView的内容是, storyboard中有两种便捷的方式.
-
Dynamic prototypes 设计的cell的prototype的目的是为了复用. 如果你的cell复用时, 使用的是相同的layout, 那么使用Dynamic, Dynamic的内容由表视图数据源(表视图控制器)在运行时以任意数量的单元格进行管理。图3-3显示了带有一个Dynamic的plain表视图。
注意:如果storyboard中的tableView是dynamic, 那么tableView一定需要一个DataSource对象, 也就是说
UITableViewController
的子类需要实现data source协议
- Static cells 使用是static cell的话, cell的内容的layout, cell的个数已经确定. TableView的cell和内容在运行前已经确定好, 是固定的. 你还是设置section header等静态信息. 当TableView的cell的layout不会改变时, 使用static cell. 如图3-4, static cell的tableView.
注意:如果storyboard中的TableView使用static cell, 那么就不需要DataSource对象了, 因为TableView的cell时确定的.
cell的重用标识符(reuse identifier)是用来标识重用的cell的, 标识符的字符串最好是能够描述cell中包含的内容, 比如cell是用来展示鸟瞰图的, 那么他的重用标识符为@"BirdSightingCell"
设计TableView的cell
前面文章有讲过, UIKit为你提供了四种类型的cell. 如果系统提供的这四种类型不能满足需求, 你可以继承UITableViewCell
来自定义cell. 具体如何自定义cell你将在接下来的文章学到, 请关注本系列文章.
TableViewCell可以拥有附加视图(Accessory view). 附加视图是UIKit提供显示在cell的右边的一个视图, 比如disclosure indicator, 使用>图标来表示, 告诉用户点击该图标可以获取更多信息.
创建多个TableView
如果你的APP中存在和管理着多个TableView, 那么往storyboard中多拖几个TableView, 可以通过将你创建的UITableViewController
子类和TableView绑定.
往工程中添加自定义类文件
- 打开Xcode, 选择File > New > File
- 在弹出的对话框中选择iOS中的cocoa touch
- 在对话框中选择Object-C类, 点击下一步
- 输入文件名称, 选择
UITableViewController
, 点击下一步 - 选择保存的路径, 点击创建
往storyboard中添加表视图控制器
- 打开要添加TableView的storyboard
- 在对象库中拖一个TableViewController到storyboard中
- 选中添加的scene, 点击utility area中的Identity button
- 在custom class组中, 选择刚创建的类
- 选择TableView的cell的style(dynamic/static)
- 往新的scene上添加一个segue
通过创建一个demo APP可以学到更多
通过Apple实战指导Your Second iOS App: Storyboards, 你可以学习如何通过storyboard技术来创建APP, 在该指导文档中, 你将会学习如何创建一个基于导航的APP, 以及如何创建使用TableView来构建界面.
使用代码创建tableView
使用UITableViewController
的好处, 能够省写很多代码, 而且可以避免一些错误.
实现DataSource和Delegate协议
使一个类成为TableView的DataSource和delegate需要遵守UITableViewDatasSource
和UITableViewDelegate
协议. 代码清单3-1展示TableView的DataSource和Delegate.
代码清单3-1 遵守DataSource和delegate协议
@interface RootViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) NSArray *timeZoneNames;
@end
创建和配置tableView
第二步是alloc, init出TableView实例. 代码清单3-2展示了创建一个plain表视图, 然后设置各种属性,frame, DataSource, delegate等信息, 在使用UITableViewController
时, 是不用设置这些属性的, 因为它自动帮你设置好了
代码清单3-2 创建一个tableView
- (void)loadView
{
UITableView *tableView = [[UITableView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame] style:UITableViewStylePlain];
tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
tableView.delegate = self;
tableView.dataSource = self;
[tableView reloadData];
self.view = tableView;
}
使用数据动态填充tableView
当TableView被创建后, TableView会收到reloadData
消息, 让TableView去访问DataSource和delegate对象, 来获取section, row等信息, 这时DataSource会告诉TableView该显示多少section和row. 之后DataSource重复调用tableView:cellForRowAtIndexPath:
方法,返回一个UITableViewCell
对象, 来告诉tableView该如何绘制row.(当你滚动tableView时, 也会导致tableView:cellForRowAtIndexPath:
被调用, 这是因为新cell会显示).
前面提过, 如果tableView是动态的(dynamic), 那么就必须实现DataSource协议, 代码清单3-3展示一个实现了DataSource和delegate协议的类.
代码清单3-3 给动态tableView填充数据
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [regions count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Number of rows is the number of time zones in the region for the specified section.
Region *region = [regions objectAtIndex:section];
return [region.timeZoneWrappers count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
// The header for the section is the region name -- get this from the region at the section index.
Region *region = [regions objectAtIndex:section];
return [region name];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyReuseIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier];
}
Region *region = [regions objectAtIndex:indexPath.section];
TimeZoneWrapper *timeZoneWrapper = [region.timeZoneWrappers objectAtIndex:indexPath.row];
cell.textLabel.text = timeZoneWrapper.localeName;
return cell;
}
在tableView:cellForRowAtIndexPath:
方法中, 出于性能考虑, DataSource会尽可能的复用cell, 首先会发送一个dequeueReusableCellWithIdentifier:
消息给到tableView, 要求TableView提供一个特定重用标识符的复用cell, 如果该cell不存在, DataSource要负责创建该cell, 并设置特定的reuse identifier. DataSource还要负责设置cell的content(上例中, 设置text)然后返回cell给TableView. 具体如何设置cell的content请看后续文章详细地看看tableView中的cell
如果你的cell是在storyboard定义的, 使用dequeueReusableCellWithIdentifier:
方法进行获取cell时总是有效的, 如果那里不需要使用复用的cell, 那么该方法会返回一个使用storyboard信息创建的新cell. 这样就不需要检查返回值是否为nil了.
使用数据动态填充tableView
如果TableView是静态的, 那么就不需要DataSource对象了, TableView在编译期配置了好, 不需要在运行期在去调用DataSource的方法了. 但是你需要使用data model中的数据来填充TableView的内容, 下面代码清单3-4就是一个这样的示例.(改代码示例来源Your Second iOS App: Storyboards)
代码清单3-4 使用数据填充静态tableView
- (void)viewDidLoad
{
[super viewDidLoad];
BirdSighting *theSighting = self.sighting;
static NSDateFormatter *formatter = nil;
if (formatter == nil) {
formatter = [[NSDateFormatter alloc] init];
[formatter setDateStyle:NSDateFormatterMediumStyle];
}
if (theSighting) {
self.birdNameLabel.text = theSighting.name;
self.locationLabel.text = theSighting.location;
self.dateLabel.text = [formatter stringFromDate:(NSDate*)theSighting.date];
}
}
上面的代码实例中, 在viewDidLoad
(该方法会等view加载到内存后调用)对tableView进行填充数据, 其中一些属性(birdNameLabel,locationLabel)是storyboard中的outlet.
填充索引列表
当需要大量按字母分类排序的数据时, 可以使用索引列表(如第一章图1-2所示). 索引列表是一种plain风格的TableView, 而且还需要对DataSource对象进行一些特殊的设置:
sectionIndexTitlesForTableView:
返回一组字符串作为index使用tableView:titleForHeaderInSection:
为每一个section指定一个index, 作为section的titletableView:sectionForSectionIndexTitle:atIndex:
指定用户点击进入相应的section的index.
在构建索引列表时, 用来填充tableView的数据结构与索引结构相符. 一般需要创建一个二维数组来保存数据. section的个数通过外层数组来反映, 内层数组代表一个section中的内容, 而且还要对这些数组按照现行索引顺序进行排序(一般按照字面表排序, 如A-Z). 可以通过类UILocalizedIndexedCollation(该类可以根据特定localization)来对二维数组排序, 这样可以简化构建顺序的二维数据这种数据结构.
模型对象需要提供一个string属性和返回string的方法来供类UILocalizedIndexedCollation
进行检验. 如果模型对象提供的是一个方法, 那么这个方法应该不需要参数. 如果一个model对象可以代表tableView中的行, 这样非常方便, 所以在定义model时, 你可以定义一个属性来保存section的index, 或者row的index. 代码清单4-5展示了这样的model的定义
代码清单3-5 设计model类
@interface State : NSObject
@property(nonatomic,copy) NSString *name;
@property(nonatomic,copy) NSString *capitol;
@property(nonatomic,copy) NSString *population;
@property NSInteger sectionNumber;
@end
在填充tableView数据之前, 你应该将数据创建加载好. 下面代码展示了数据的加载和数据的排序处理
代码清单3-6 loading数据
- (void)viewDidLoad {
[super viewDidLoad];
UILocalizedIndexedCollation *theCollation = [UILocalizedIndexedCollation currentCollation];
self.states = [NSMutableArray arrayWithCapacity:1];
NSString *thePath = [[NSBundle mainBundle] pathForResource:@"States" ofType:@"plist"];
NSArray *tempArray;
NSMutableArray *statesTemp;
if (thePath && (tempArray = [NSArray arrayWithContentsOfFile:thePath]) ) {
statesTemp = [NSMutableArray arrayWithCapacity:1];
for (NSDictionary *stateDict in tempArray) {
State *aState = [[State alloc] init];
aState.name = [stateDict objectForKey:@"Name"];
aState.population = [stateDict objectForKey:@"Population"];
aState.capitol = [stateDict objectForKey:@"Capitol"];
[statesTemp addObject:aState];
}
} else {
return;
}
当数据加载完后, 可以使用UILocalizedIndexedCollation
类对数据进行处理
代码清单3-7 对数据进行索引化
// viewDidLoad continued...
// (1)
for (State *theState in statesTemp) {
NSInteger sect = [theCollation sectionForObject:theState collationStringSelector:@selector(name)];
theState.sectionNumber = sect;
}
// (2)
NSInteger highSection = [[theCollation sectionTitles] count];
NSMutableArray *sectionArrays = [NSMutableArray arrayWithCapacity:highSection];
for (int i = 0; i < highSection; i++) {
NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:1];
[sectionArrays addObject:sectionArray];
}
// (3)
for (State *theState in statesTemp) {
[(NSMutableArray *)[sectionArrays objectAtIndex:theState.sectionNumber] addObject:theState];
}
// (4)
for (NSMutableArray *sectionArray in sectionArrays) {
NSArray *sortedSection = [theCollation sortedArrayFromArray:sectionArray
collationStringSelector:@selector(name)];
[self.states addObject:sortedSection];
}
} // end of viewDidLoad
对上面代码做一些说明:
- 遍历所有model数据, 调用collation对象的
sectionForObject:collationStringSelector:
方法, 该方法的参数是一个model对象, 和一个model对象属性或者定义的方法(用来进行检验的), 然后该方法会返回一个处于哪个section的index. 将前面的index设置为model的sectionNumber
- 创建一个section数组(外层), 保存每一个section中的数据.
- 循环遍历一个模型数组中的每个数据, 将其加入相应的section数组(内存)中.
- 然后调用collation manager的
sortedArrayFromArray:collationStringSelector:
方法来对section数组(外层)中的内层数组排序
经过上面的步骤后, DataSource已经将数据准备好, 下面是具体设置列表的index, 代码清单3-8展示了这一过程, 该过程调用了UILocalizedIndexedCollation
类的两个方法sectionIndexTitles
和sectionForSectionIndexTitleAtIndex:
来完成任务, 注意在方法tableView:titleForHeaderInSection:
方法中, 会将空的section从列表中移除.
代码清单3-8 为tableView提供section-index数据
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if ([[self.states objectAtIndex:section] count] > 0) {
return [[[UILocalizedIndexedCollation currentCollation] sectionTitles] objectAtIndex:section];
}
return nil;
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
return [[UILocalizedIndexedCollation currentCollation] sectionForSectionIndexTitleAtIndex:index];
}
最后实现DataSource协议方法, 如代码清单3-9所示
代码清单3-9 填充索引列表中的行
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [self.states count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[self.states objectAtIndex:section] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"StateCell";
UITableViewCell *cell;
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
State *stateObj = [[self.states objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
cell.textLabel.text = stateObj.name;
return cell;
}
如果你的TableView是索引列表, 那么请注意, 你应该将cell的accessoryType
设置为UITableViewCellAccessoryNone
.
经过上面的一个步骤轮廓, 你可以调用reloadSectionIndextitles
方法来reload列表的index
tableView的可选性配置
对于tableView, UIKit提供了很多Api来对tableView进行配置, 可以在外观和行为上对tableView进行各种控制, 比如控制section, 控制row等等. 下面就这些配置提供几个方向给你选择.
添加一个自定义标题
在对tableView进行配置时, 使用UITableView
类的方法对tableView配置是全局性的. 下面代码展示了如何为tableView添加一个自定义的title(使用UILabel
)
代码清单3-10 为tableView添加一个标题
- (void)loadView {
CGRect titleRect = CGRectMake(0, 0, 300, 40);
UILabel *tableTitle = [[UILabel alloc] initWithFrame:titleRect];
tableTitle.textColor = [UIColor blueColor];
tableTitle.backgroundColor = [self.tableView backgroundColor];
tableTitle.opaque = YES;
tableTitle.font = [UIFont boldSystemFontOfSize:18];
tableTitle.text = [curTrail objectForKey:@"Name"];
self.tableView.tableHeaderView = tableTitle;
[self.tableView reloadData];
}
提供一个section表示
代码清单3-11 返回section的title
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
// Returns section title based on physical state: [solid, liquid, gas, artificial]
return [[[PeriodicElements sharedPeriodicElements] elementPhysicalStatesArray] objectAtIndex:section];
}
缩进一行
代码清单3-12 设置行的缩进
- (NSInteger)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath {
if ( indexPath.section==TRAIL_MAP_SECTION && indexPath.row==0 ) {
return 2;
}
return 1;
}
改变一行的高度
代码清单3-13 控制不同行的行高
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat result;
switch ([indexPath row])
{
case 0:
{
result = kUIRowHeight;
break;
}
case 1:
{
result = kUIRowLabelHeight;
break;
}
}
return result;
}
自定义Cell
关于要自定义cell, 那么主要的操作集中在DataSource的tableView:cellForRowAtIndexPath:
方法中, 返回一个自定义的UITableViewCell
子类的cell, 具体内容请看下篇文章详细地看看tableView中的cell