利用列表视图在数据结构中导航
列表视图的常见用法——也是它们能够完美胜任的地方——是导航拥有层级结构的数据。在数据结构最顶端的列表罗列出最基本层次的数据种类。用户选择一行,在结构中下探(drill down
)至下一个层级。在结构的底部,会有一个视图(通常是一个列表视图)显示关于用某个条目的具体细节信息(例如,一个电话本的通话记录),而且还可能允许用户条目进行编辑。本节将会讲解如何让数据模型层级结构中的各个层次分别对应不同列表视图,以及如何使用UIKit框架所提供的功能来帮助读者实现导航功能的应用。
层级数据模型和列表视图
对于导航型的应用来说,一般将应用数据设计为被称为数据模型
的对象关系图。然后就可以利用各种机制和技术实现模型的每一层数据结构,包括Core Data,属性列表,或自定义对象的归档文件。不管哪种方法,遍历应用数据模型所遵循的模式对于所有的导航型应用都是类似的。数据模型有结构深度,处在不同深度的对象应该能够被定位和找到,以便用来填充列表视图的行。
注释:要了解更多关于
Core Data
框架和其技术的信息,详见Core Data Starting Point。
拥有层级结构的数据模型
一个设计良好的应用在构建其类和对象的时候会遵守MVC设计模式。应用的数据模型在这个模型中扮演模型对象的角色。读者可以用模型对象的属性来描述它们。这些属性氛围两类:attribute
和relationship
(特性和关系)。
注释:这里所提到的“属性”对应于,但不相同,Objective-C语言的属性特性。类定义中通常用实例变量和被声明的属性来表示类的属性。
特性代表模型对象数据的元素。特性可以是包装基本数据类型的类的对象(例如,NSString,NSDate,UIColor对象),也可以是一个C语言结构体或一个标量值。特性一般来说用来填充代表数据结构中节点的列表视图,或者代表条目细节信息的视图。
(模型对象不止一个)一个模型对象会同会和其他模型对象有联系。,同对象之间的复合,这些联系构成了数据模型的结构深度。根据对应关系的不同,关系可以氛围两类:to-one
和to-many
(一对一和一对多)。一对一关系定义了一个对象同另一个对象的关系(例如,父子关系)。一对多关系,从另一方面来说,定义了一个对象同多个同类对象之间的关系。一对多关系最典型的特征就是包含
,在代码中常常通过集合类,例如NSArray
对象(或数组)来表示。数据可以包含其他数据,或者包含多个字典,字典是利用通过键识别对应的值的集合。字典,相应的,也可以包含多个其他集合,包括数组,无序集合,甚至其他字典。随着集合多个集合类的互相嵌套,数据模型就获得了相应的结构深度。
列表视图和数据模型
平实风格列表的行通常代表数据模型中的集合对象;这些对象通常是数组。数组包含字符串或其他元素,列表将其作为内容显示在行内。当创建一个列表视图时(详见创建和配置列表视图),其会立即向数据源查询自己的尺寸——即,请求节的数量,每节中行的数量——然后请求每行所需显示的内容。数据源从数据结构中对应的层级(数组)中取回这些内容,将它们传递给列表视图。
在许多为列表视图的数据源和代理所定义的方法中,列表视图会传入一个索引路径来指明当前操作所对应的节和行的位置——例如,获取某行所显示的内容或者指明用户所点击的行。所以路径是Foundation框架类NSIndexPath
类的实例对象,专门被用来表示在多个嵌套数组中的某个项目的位置。UIKit框架为NSIndexPath
类增加了类扩展,为期添加了row
和section
属性。数据源应该使用这些属性来将列表的行和节同数据结构中作为数据源使用的数组中的相应的数据值对应起来。
注释:UIKit框架对于
NSIndexPath
的扩展,详见NSIndexPath UIKit Additions。
在图3-1所展示的列表序列中,数据结构的最上层,是一个拥有四个数组的数组,,每个被包含的数组又包含若干个代表特定地区旅游线路的对象。当用户选中一个地区时,下一个列表视图会罗列出被选中数组内的所有的旅游线路的名称。当用户选中一个特定路线时,下一个列表视图会利用聚合列表提供线路的详细信息。
注释:可以对图3-1的应用进行改动,使其只有两个列表视图。第一个列表视图是一个索引列表,根据地区罗列出线路。第二个视图显示线路的详细信息。
视图控制器和导航型应用
UIKit框架提供了数个视图控制器类来管理iOS中常见的交互界面形式。视图控制器是继承自UIViewController
类的控制器对象。它们是视图管理的重要工具,特别是当应用使用视图来连续显示数据结构中的各个层次时。本节将讲解两个UIViewController
的子类,导航控制器和列表控制器,以及它们是如何呈现和管理一组连续列表视图的。
注释:本节是对视图控制器的概述,为稍后章节的编码任务提供一些背景知识。更多关于视图控制器的信息,详见View Controller Programming Guide for iOS。
导航控制器
UINavigationController
类继承自UIViewController
类,后者是一个基础类,定义了iOS中用于管理视图的控制器对象的通用编程接口和行为。通过继承自这个基础类,视图控制器可以获得视图管理的通用功能。实现这些功能的一部分后,视图控制器可以自动旋转它的视图,响应低内存警告,覆盖显示modal
(模态)视图,响应对编辑键的点击,以及其他管理视图的功能。
导航控制器管理着一个由视图控制器组成的栈,每次只能显示一个栈中的控制器的列表视图(见图3-2)。最开始的被称为根视图控制器
。当用户点击列表中的一行(通常是点击细节扩展按钮),跟视图控制器将下一个视图控制器推至栈的最上方。新的视图控制器的视图会从屏幕的右方滑动至屏幕,屏幕上方的导航栏项目会被更新。当用户点击导航栏的后退键时,当前视图控制器被从栈中弹出。随后,导航控制器会显示当前处于栈最顶端的控制器的视图。
导航栏
导航栏是一个用户界面交互装置,允许用户在数据结构中导航。用户从笼统的数据结构最上层开始,下探至显示某个节点的特定属性的细节视图。导航栏的下方所显示的视图代表当前的数据层次。导航栏包括一个当前视图的标题,而且,如果视图代表的不是数据结构结构的最上层,导航栏的左侧会显示一个返回键;返回键是一个导航控件,用户点击它可以返回前一个层次。(返回键默认显示为前一个视图的标题。)导航栏可能还有编辑键——用来进入当前视图的编辑模式——或者其他自定义的管理视图内容的按键。(键图3-3)
导航控制器负责管理导航栏,包括在其中所显示的同下方视图相关的控件。UIViewController
对象管理着显示在导航栏下方的视图。对于这个控制器而言,它是UIViewController
类或者UIKit框架所提供的管理某一特定类型的视图的视图控制器类的子类。对于列表视图来说,这个视图控制器类是UITableViewController
。对于一个显示一系列代表不同数据层级的列表视图的导航控制器来说,需要为每个列表视图都创建单独的自定义视图控制器。
UIViewController
类包括了可以让视图控制器访问和设置显示在导航栏中的关于当前列表视图的项目的方法。这个类同样还声明了一个title
属性,可以通过设置这个属性来让导航栏显示与当前视图对应的标题。
列表视图控制器
虽然可以使用UIViewController
类的子类来管理列表视图,但是如果使用UITableViewController
的子类的话,将省去很多麻烦。UITableViewController
类已经实现了许多功能,这是UIViewController
类的子类所不具备的。
创建列表视图控制器的推荐方法是使用故事面板。关联的列表视图会自动被载入故事面板,以及视图的属性,尺寸,自动缩放特性。列表视图控制器会将自己设置为列表视图的数据源和代理。
注释:也可以通过内存分配初始化的方式利用代码创建列表视图控制器,所使用的方法是
initWithStyle:
,传入UITableViewStylePlain
或UITableViewStyleGrouped
两个中的一个作为参数。
当列表视图即将第一次出现在屏幕上时,列表视图控制器会向其发送reloadData
消息,这会促使视图向其数据源请求数据。数据源会告知列表视图其由多少节和多少行需要显示,然后将每行所需显示的数据给到列表视图。这个过程详见创建和配置列表视图。
UITableViewController
类还可以执行其他任务。它可以在视图即将显示之前清除选中效果,视图结束显示时刷新滚动指示器。除此之外,它还可以在用户点击编辑按钮时让列表视图进入编辑模式(或在用户点击完成按钮时让视图推出编辑模式)。这个类有一个tableView
属性,通过它可以访问被管理的列表视图。
注释:列表视图控制器支持列表行的行内编辑;例如,如果在编辑模式下行内有内嵌的文本框,它会将被编辑的行滚动至虚拟键盘上方。另外还支持
NSFetchedResultsController
类来管理来自Core Data
抓去请求返回的结果。
UITableViewController
类通过覆盖loadView
,viewWillAppear
等继承自UIViewController
类的方法来实现前述行为。在自定义的UITableViewController
子类中,必须覆盖这些方法来获得特定的行为。如果需要需钙这些方法,无比要调用父类的实现,通常在实现代码的第一行中,以便首先得到默认的行为。
注释:如果视图由多个子视图组成,而其中只有一个是列表视图,那么就要用
UIViewController
的子类而非UITableViewController
的子类来管理这个视图。UITableViewController
类的默认会让列表视图填满导航栏和标签栏之间的整个区域(两者如果显示的话)。
如果决定使用`UIViewController`的子类而非`UITableViewController`的子类来管理列表视图,就需要上面所提到的几个任务,使得视图符合界面设计指南(`HIG`)。为了在视图显示前清除选中效果,必须在`viewWillAppear:`方法的实现内通过调用`deselectRowAtIndexPath:animated:`来清除被选中的行(如有)。列表视图被显示之后,应该通过向其发送`flashScrollIndicators`消息来刷新滚动视图的滚动指示器;在`vieDidAppear:`中的实现内来调用这个方法。
在导航型应用中管理列表视图
UITableViewController
对象——或任何其他扮演列表视图数据源和代理的对象——必须响应列表视图向其发送的消息,这些消息包括为了填充视图的行,配置视图,响应选中,以及管理编辑模式。接下来,读者将会学到如何完成这些任务。然而,想要在一个导航型的应用中恰当的显示由多个列表视图组成的序列,还有些东西需要了解。
注释:本节总结了视图控制器和导航控制器的任务,主要围绕列表视图。为了更好的了解视图控制器和导航控制器,包括它们实现的完整细节,详见View Controller Programming Guide for iOS和View Controller Catalog for iOS。
此时,我们假设一个被列表视图控制器所管理的列表视图向用户呈现了一个清单。那么应用应该如何显示视图序列中的下一个列表视图呢?
当用户点击列表的某行时,列表视图会调用代理所实现的tableView:didSelectRowAtIndexPath:
或tableView:accessoryButtonTappedForRwoWithIndexPath:
方法。(如果用户点击了行的细节扩展按钮,就会调用后一个方法)。代理会创建序列中管理下一个列表视图的列表视图控制器,为期 设置所需填充列表的数据,然后将这个控制器推送至导航控制器栈的最顶端。故事面板显示这方面的细节,允许UIKit替你完成大部分工作。
故事面板代表应用中的scene
(场景)和场景之间的转换。在一个基本的应用中故事面板可能会包含几个场景,但在复杂的应用中可能会包含多个故事面板,每个都包含一组子场景。图3-4显示了一个故事面板,其每个场景都用图形界面表示了出来,包括内容和尝尽之间的关系。
场景代表由视图控制器管理的屏幕上的内容区域。(在故事面板中,场景和视图控制器是相似的术语)。故事面板中最左边的默认场景代表导航控制器。导航控制器是一个容器控制器,因为除了自己的视图意外,它还管理着一组其他视图控制器。例如,图3-4的导航控制器负责管理主视图和细节视图控制器,除了它本身所管理的导航栏,返回键。
关系代表了常见之间连接的类型。在图3-4中,导航控制器和主界面之间有一个关系。在这种情况下,关系代表导航控制器包含主视场景和细节场景。当应用运行时,导航控制器自动载入主场景和细节场景,并且在屏幕的上部显示导航栏。
segué
(转场)代表从一个场景(称为起点)到另一个场景(称为终点)的过度。例如,图3-4中,主场景是起点,细节场景是终点。当在主场景的列表中选择某个项目时,就出发了从起点到终点的转场。在这种情况下,这个转场是一个push segue
(推进转场),意味着终点场景从右至左划入屏幕覆盖起点场景。正如细节视图所显示的,导航栏左边有一个返回键,其标题为上一个场景的标题(在这个例子中是“Master”)。返回键由管理整个主-细结构的导航控制器提供和管理。
通过UIViewController
类的prepareForSegue:sender:
方法,故事面板使得把数据从一个场景传递至另一个场景变得简单。这个方法在第一个场景(起点)即将切换至下一个场景(终点)时被调用。起点视图控制器可以通过实现prepareForSegue:sender
来设置任务,例如将信息传递至终点视图控制器,这些信息就是终点视图需要显示的。表3-1显示了实现这个方法的一个例子。
- (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];
}
}
转场代表从起点场景到终点场景的单向过度。这样设置的结果之一是因为可以使用转场向终点传递数据,但不能利用转场将把数据从终点传递到起点。为了解决这个问题,可以创建一个代理协议,声明当终点视图控制器需要回传数据使可以调用的方法。
表3-2显示了这样一个协议中的其中一个方法的实现,这个方法允许终点视图控制器回传数据。
@protocol MyAddViewControllerDelegate
- (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];
}
注释:关于创建故事面板的全部细节,详见Xcode Overview。更多关于在故事面板中使用视图控制器的信息,详见View Controller Programming Guide for iOS。
导航型应用的设计模式
使用列表视图的导航型应用应用遵守以下设计规范:
-
视图控制器(一般是
UITableViewController
子类),扮演数据源的橘色,利用来自数据结构中特定层次的对象的数据填充它的列表视图。当列表视图显示一组条目时,那么提供这些数据的数据结构中的那个对象一般是一个数据。当列表视图显示条目详情时(即数据结构中的节点的详细信息),那个对象可能是一个自定义的模型对象,一个由Core Data管理的对象,字典,或其他类似的对象。
-
视图控制器会存储其用来填充列表视图的数据
视图控制器可以使用这些数据来直接填充列表视图,或者用来抓取或获得所需的数据。在设计自定义控制器类时,应该为控制器定义一个属性,以便保存这些数据。
视图控制器不应该通过全局变量(
Global variable
)或单例对象(Singleton
)(例如应用代理appDelegate
)获取给列表视图的数据。这种直接的依赖关系会让代码的可重复利用性大大降低,并且很难测试和排查bug。 当前处于导航控制器的栈最上方的控制器会根据控制器序列创建下一个控制器,为这个控制器设置其所需的用来填充自己列表视图的数据,这一连串动作发生在其被弹出栈并将下一个控制器推至栈顶端之前。