iOS TableView 编程指导(二)-使用TableVie
tableView的一个常用, 也是最适用的用法是层级化数据的导航浏览. 使用一个TableView来显示数据的第一层, TableView中的每一行都代表一类数据. 点击某一行会继续展示下层的数据, 数据数的最底层通常也是用一个TableView来显示, 显示数据的详细信息(比如, 通讯录中的具体联系人信息), 同时还能编辑该详细信息. 本文内容是关于TableView如何展示层级数据的.
分层数据模型和表视图
对于基于导航的APP来说, 你通常会将APP的数据设计为模型对象图, 该模型对象有时称为APP的数据模型. 你可以使用Core Data, property lists, 或自定义对象的archiives
等技术来实现APP的model层. 无论使用何种技术, 基于导航的APP中的数据处理的方法时一样的. 数据模型是有层级深度的, 表视图中每行的数据来源于各层的数据对象.
数据模型是作为模型对象的层次结构
设计良好的App中类和对象应该遵守模型-视图-控制器
(MVC)设计模式. APP中的数据模型由该模式中的数据对象组成. 您可以根据对象的属性描述模型对象(使用对象建模模式提供的术语). 这些属性有两大类:特征(attributes)和关系(relationship)
注意: 这里
property属性
的概念和OC中的属性
概念有关系, 但不完全相同. OC中的属性是通过类定义时声明属性或者实例变完成的.
特征(attributes)是数据模型的组成元素. attributes的范围可以是类中的属性, C结构体, 标量值. 这些attributes通常用来填充表视图, 代表数据层级树中的叶节点, 是一项对象的具体信息.
一个模型数据可以和其他模型对象建立联系. 有两种关系:一对一, 一对多. 一对一关系指一个对象和另外一个对象建立联系, 比如(父子关系). 一对多关系指一个对象可以和多个其他对象建立联系, 通常使用集合对象来表示(数组, 字典, set)
表视图和数据模型
plain表视图中行的数据一般来源于APP数据集合, 通常使用数组来表示. 数组可以包含字符串等用来展示的数据内容. 当创建TableView时, 需要立即从DataSource中获取相应section和相应行的具体数据.
在DataSource和delegate的许多方法中, 用到了index path来定位tableView的row和section, index path是类NSIndexPath
实例, 用来表示多层树中的item, UIKit扩展了该类, 使得可以直接使用section
和row
属性.
在图2-1的表视图中, 第一层显示的包含四个元素的数组, 每一个元素又对应一个数组(该数组包含的元素代表底层地区中包含的路线). 当用选择一个地区时, 第二tableView将会显示该地区中的路线. 当用户选择某条路线时, 将会展示第三个tableView.
图2-1 使用tableView展示三层数据注意:上图2-1中, 你也可以使用两个tableView来展示, 第一个tableView使用按照地区索引展示每个地区的路线, 第二TableView展示具体路线的详细信息.
视图控制器和基于导航的应用程序
iOS中, UIKit提供了许多视图控制器来满足用户界面需求. 试图控制器都是类UIViewController
的实例, 用来管理视图的. 下面讲解UIViewController
的两个子类UINavigationController
(导航控制器)和UITableViewController
(表视图控制器)来展示和管理一连串的TableView.
导航控制器
我们都知道UIViewController
是用来管理用户界面视图-view的, 它可以响应事件, 屏幕旋转, 处理低内存警告等, 它提供了管理视图界面的一系列基本借口. 而UINavigationController
是它的子类, navigationController内部会维持一个Controller栈, 如图2-1, 栈中保存了一组控制器, 控制器内包含TableView. navigation Controller是从它的rootViewController
开始的, 当用点击了TableView的某一行, 会push一个controller到controller栈中, 此时界面的效果是从屏幕的右边出来一个新的TableView, 当你点击导航栏上的back按钮时, 会从controller栈pop出第一个controller, 界面的显示第一个TableView.
Navigation Bars
导航栏是用户界面的一种装备, 能够使用户浏览数据的层次结构. 用户从顶层开始, 然后向下'钻取'层级结构以显示叶节点项的详细信息. 导航栏下方视图展示的是当前层次数据. 导航栏包括一个当前视图的标题, 如果展示的视图不是顶层视图, 那么会显示一个back按钮, 点击back按钮会向上一层显示. 另外导航栏还可以有一个编辑按钮, 用于进入当前视图的编辑模式, 或者显示自定义的导航按钮. 参考图2-3.
图2-3 导航栏和常用控制按钮
UINavigationController
管理导航栏, 包括导航栏上的bar item. UIViewController
管理的view显示在导航栏的下方, 对于这个视图控制器,您可以创建UIViewController的子类或视图控制器类的子类,UIKit框架提供了这些子类来管理特定类型的视图。对于表视图,此视图控制器类是UITableViewController。对于显示反映数据层次结构中级别的表视图序列的导航控制器,需要为每个表视图创建单独的自定义表视图控制器。
UIViewController类包括允许视图控制器访问和设置当前显示的表视图的导航栏中显示的导航项的方法。该类还声明一个title属性,通过该标题属性可以设置当前表视图的导航栏的标题。
Table View Controllers
尽管你可以使用UIViewController
或者它的子类来直接管理tableView, 但是如果你使用UITableViewController
的话, 会更方便, 该控制器帮你干了许多额外的活儿. tableView controller会将TableView的delegate和DataSource设置为自己.
注意:除了使用storyboard外, 你可以使用代码创建UITableViewController, 使用
initWithStyle:
初始化方法来创建, 传人的参数是UITableViewSytlePlain
或者UITableViewStyleGrouped
当TableView准备开始显示时, controller会调用TableView的reloadData
方法, 该方法会让TableView从DataSource中获取section和row的行数和内容信息. UITableViewController
还做一些额外的工作. 当tableView开始显示时, 会清除TableView的选中; 当TableView结束显示时会闪动indicator. 另外, 当用户点击controller的编辑按钮时, TableView会进入/退出编辑状态. UITableViewController
会暴露tableView
属性来给你操作.
注意:UITableViewController还支持行内编辑; 列如, 在当行内的textField处于编辑模式时, 控制器会将TableView滚动到键盘的上方. 还支持NSFetchedResultsController类, 用于管理从Core Data中查询的结果.
UITableViewController
通过重写UIViewController
中的loadView
,viewWillAppear:
等方法来实现上述的功能. 如果你需要继承UITableViewController
, 那么你需要重写这些方法, 在这些方法中需要调用super的方法.
注意:如果你的控制器需要管理好几个view不是但一个TableView, 你就应该继承
UIViewController
而不是UITableViewController
.UITableViewController
默认是TableView填充navigationBar和tabBar之间的屏幕内容.
如果你使用UIViewController
的类话, 你需要实现前面说的协议方法等工作, 另外如果你需要清除选中的row的话, 你就需要重写viewWillAppear:
, 在方法中调用tableView的deselectRowAtIndexPath:animated:
方法. 在tableView显示完成后, 需要在viewDidAppear:
中给tableView发送flashScrollIndicator
消息来闪动indicator.
管理基于导航的APP中的表视图
UITableViewController
对象或者其他tableView的DataSource和delegate对象需要支持一些能力,包括section, row信息, 响应选中row, 管理编辑会话.
到了此刻, 假设controller管理tableView显示, 那么APP如何显示下一个TableView呢?
当用户点击row时, TableView会调用delegate的tableView:didSelectRowAtIndexPath:
或tableView:accessoryButtonTappedForRowWithIndexPath:
(当用户点击detail disclosure按钮时)方法. 然后, delegate创建一个tableviewController, 设置第二TableView的DataSource和delegate, 然后push出来即可. 使用storyboard创建的话, 会更直观如图2-4
storyboard通过prepareForSegue:sender
方法来传递数据给另外一个控制器. 当一个控制器跳转到另外一个控制器前会调用该方法, source controller可以用prepareForSegue:sender
来实现任务的setup, 将信息传递给destination controller. 代码清单2-1展示了该方法的实现.
代码清单2-1 传递数据到destination controller
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"ShowDetails"]) {
MyDetailViewController *detailViewController = [segue destinationViewController];
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
detailViewController.data = [self.dataController objectInListAtIndex:indexPath.row];
}
}
storyboard中segue代表source场景到destination场景的跳转. 所以你可以使用segue来传递数据给destination, 但是你不能通过segue将数据传回给source. 为了解决这个问题, 你需要创建一个delegate protocol, 该protocol声明一个方法, 用来将数据传回去.
代码2-2展示如何实现该用来传回数据给source controller的protocol
代码2-2 传回数据给到source controller
@protocol MyAddViewControllerDelegate <NSObject>
- (void)addViewControllerDidCancel:(MyAddViewController *)controller;
- (void)addViewControllerDidFinish:(MyAddViewController *)controller data:(NSString *)item;
@end
- (void)addViewControllerDidCancel:(MyAddViewController *)controller {
[self dismissViewControllerAnimated:YES completion:NULL];
}
- (void)addViewControllerDidFinish:(MyAddViewController *)controller data:(NSString *)item {
if ([item length]) {
[self.dataController addData:item];
[[self tableView] reloadData];
}
[self dismissViewControllerAnimated:YES completion:NULL];
}
基于导航的APP中的设计模式
一个包含TableView的导航APP需要遵守下面的设计规范:
- 视图控制器(通常是UITableViewController的子类)充当数据源的角色, 使用来自表示数据层次结构的模型对象的数据来填充其表视图
当表视图显示项目列表时, 对象通常是一个数组。当表视图显示项目详细信息(即,数据层次结构的叶子节点)时, 对象可以是自定义模型对象、Core Data管理对象、字典或类似的对象。 - 视图控制器存储填充表视图所需的数据
视图控制器可以直接使用这些数据来填充表视图, 或者使用它来获取或以其他方式获取必要的数据. 当设计视图控制器子类时, 应该定义一个属性来保存这些数据。
视图控制器获取数据时, 不应该通过全局变量或单例对象来作为delegate. 这样的直接依赖性使得代码不可重用, 更难测试和调试. - 在push下一个试图控制器, 需要先创建TableView的controller, 并设置TableView的DataSource和delegate.