iOS系列译文:整洁的表视图代码

本文由 伯乐在线 - christian 翻译自 Florian Kugler。欢迎加入技术翻译小组。转载请参见文章末尾处的要求。

表视图是一个非常万能的iOS应用程序构建模块。因此,有很多与表视图直接或间接相关的代码,包括提供数据、更新表视图、控制其行为和选择做出的反应,这仅仅是几个例子。在这篇文章里,我们将会介绍一些整洁而结构良好的代码。

 

UITableViewController VS UIViewControler

苹果提供了UITableViewController作为表视图的专用视图控制器。Table view controllers实现了一些非常有用的特性来帮助你避免写重复的代码。另一方面,Table view controllers仅限于管理一个全屏显示的表视图。然而,在大多数情况下,这就可以满足你的需要了。如果不满足的话,我们将会在下面讲解解决它的方法。

 

Table View Controllers的特性

Table View Controllers将会在它第一次显示的时候帮你加载表视图的数据。更具体地说,它可以帮助切换表视图的编辑模式,对键盘的通知做出反应,比如滚动刷新和清除选项这类的小功能。重要的是,你可以自定义子类来重写这些可以被称为万能的视图时间方法来实现这些特性。

Table View Controllers有一个独特的卖点超越了标准视图控制器的独特卖点,它支持苹果的z“下拉更新”的功能目前,这是唯一通过使用一个表控制器来控制刷新记录的方法。还有一些其他的方法来使它生效,但是在下一次iOS中的更新却不是那么的容易

所有的这些原理提供了大量的表视图控制器的接口像苹果已经定义的那样,如果你APP符合这些标准,坚持使用表视图控制器是一个避免重写模版代码的好办法。

 

Table View控制器的限制

表视图控制器的视图属性总是被设定在一个表视图上。如果你以后决定想在屏幕上显示除了视图表以外的东西(比如地图),假如你不想依赖于笨拙的补丁那你就有的惨了。

如果你已经定义在代码里定义了接口或者在使用.xib文件,那么转换到一个标准的视图控制器就会相当容易了。如果你使用脚本的话这个转换过程会涉及到更多一些的步骤。用脚本,你需要通过重新创建来把一个表视图控制器转变为一个标准的表视图控制器。这就意味这你必须把所有的内容复制到这个新的试图控制器中然后再重新建立。最后,你需要重新把转变过程中丢失的表视图控制器的功能添加一遍。大部分都是viewWillAppear或viewDidAppear里简单的单行语句。切换编辑状态需要一个点击表视图的编辑属性方法来执行。大部分的工作在于重新创建键盘支持。

在你继续走这个路线之前,这里有一个关注点分离附加好处的简单替代方法。

 

子视图控制器

并非为了完全摆脱表视图管理器,你也可以将其作为一个子视图控制器添加到另一个视图控制器(见本文关于视图管理器的控制)。然后表视图管理器只需要继续管理这个表视图而父视图管理器可以处理任何额外你需要的界面元素。

1
2
3
4
5
6
7
8
9
10
11
12
- (void)addPhotoDetailsTableView
{
    DetailsViewController *details = [[DetailsViewController alloc] init];
    details.photo = self.photo;
    details.delegate = self;
    [self addChildViewController:details];
    CGRect frame = self.view.bounds;
    frame.origin.y = 110;
    details.view.frame = frame;
    [self.view addSubview:details.view];   
    [details didMoveToParentViewController:self];
}

如果你选择这个解决方法的话,你需要建立一个子视图和父视图之间的通信通道。例如,为了能够push另一个视图进来,父视图需要知道table view的cell被选中了。鉴于这个使用场景,最干净的方法就是为table view控制器定义一个代理协议,可以在父视图中实现这个协议。

如果你要使用这个解决方法,你必须创建一个从子类到父类的通信通道。例如,如果用户选择了一个表视图的单元格,父视图控制器需要接收到消息来推动另一个视图控制器。根据实例,通常最简洁的方法是为这个表视图控制器定义一个委托协议,然后你在父视图管理器中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@protocol DetailsViewControllerDelegate
- (void)didSelectPhotoAttributeWithKey:(NSString *)key;
@end
 
@interface PhotoViewController () <DetailsViewControllerDelegate>
@end
 
@implementation PhotoViewController
// ...
- (void)didSelectPhotoAttributeWithKey:(NSString *)key
{
    DetailViewController *controller = [[DetailViewController alloc] init];
    controller.key = key;
    [self.navigationController pushViewController:controller animated:YES];
}
@end

正如你看的那样,这种结构会在视图控制器之间的通信中伴随一些其他的额外开销来换取干净的关注点分离和更好的重用性.根据具体的用例,最终使事情比必需要更简单或者更复杂,这是你需要考虑和决定的。

 

分离关注点

当处理表视图时有各种不同的任务关于模型,控制器和视图的跨越边界问题。为了防止视图控制器成为存放这些任务的地方,我们会视图讲这些任务单独的放在更合适的地方。这有助于代码的可读性,维护性和测试性。

在轻视图控制器的文章里讲述了详细的概念和扩展技术。如何把我们的数据源和模型逻辑引入。在表视图的环境下,我们将专门看看如何分离视图控制器和视图的关注点的问题。

 

桥接模型对象和单元之间的差距

在某种程度上,我们必须交出想要在视图层显示的数据。由于我们想要维持一个模型和视图之间清晰的分离点,经常把这个任务放到表视图的数据来源里。

1
2
3
4
5
6
7
8
9
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PhotoCell"];
    Photo *photo = [self itemAtIndexPath:indexPath];
    cell.photoTitleLabel.text = photo.name;
    NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
    cell.photoDateLabel.text = date;
}

这个代码将数据源和cell的设计逻辑绑定在了一起。我们最好将这个在cell的类别类里重构一下。

1
2
3
4
5
6
7
8
9
10
@implementation PhotoCell (ConfigureForPhoto)
 
- (void)configureForPhoto:(Photo *)photo
{
    self.photoTitleLabel.text = photo.name;
    NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
    self.photoDateLabel.text = date;
}
 
@end

这种情况下,我们的数据源代码就变得非常简单了。

1
2
3
4
5
6
7
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier];
    [cell configureForPhoto:[self itemAtIndexPath:indexPath]];
    return cell;
}

在示例代码中,初始化cell的时候时候block的方式,table视图的数据源已经分离到了一个单独的控制对象中,在这个例子中,这个block就像下面这样

1
2
3
TableViewCellConfigureBlock block = ^(PhotoCell *cell, Photo *photo) {
    [cell configureForPhoto:photo];
};

 

使cell可重用

在这种有多个数据模型使用同一个cell类型展示的情况下,我们甚至可以进用一步就可以达到cell重用的效果。首先,我们定义一个所有需要使用这个cell类型展示数据的对象都需要实现的协议。然后,我们修改一些cell类别中的配置方法,使它可以接受任何遵循上述协议的对象。这两个简单的步骤将cell和数据模型分离并且使cell可以接受不同的数据类型。

在cell中处理cell的状态

如果我们想做一些与默认情况下不同的table view的高亮和选中状态,我们需要实现两个代理方法来实现将cell修改成我们想要的状态。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)tableView:(UITableView *)tableView
        didHighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
    cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
}
 
- (void)tableView:(UITableView *)tableView
        didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.photoTitleLabel.shadowColor = nil;
}

然而,这两个方法需要依赖于知道cell是如何布局的,如果我们想要换一个cel或者重新设计cell,我们同样需要修改这段代理代码。view的设计细节就和代理交织在一起了,我们应该将这段逻辑放到cell中。

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation PhotoCell
// ...
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
    [super setHighlighted:highlighted animated:animated];
    if (highlighted) {
        self.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
        self.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
    } else {
        self.photoTitleLabel.shadowColor = nil;
    }
}
@end

一般来说,我们强烈建议将view层的实现细节和控制器层的实现细节分离开来。代理可以知道view的状态变化,但是不应该知道如何修改view的树状结构以及它的子视图应该设成什么状态,所有这些状态都应该封装在view中,然后提供给外部一个访问的接口。

 

处理多种cell类型

如果在一个table view中有多种cell类型,数据源就要变得失控了。在我们的示例app中,我们的照片详情表格有两种不同类型的cell:一个显示评分,另一个就是一般的显示键-值的cell。为了将显示不同cell类型的代码分离,数据源方法里就是简单调用不同类型cell的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *key = self.keys[(NSUInteger) indexPath.row];
    id value = [self.photo valueForKey:key];
    UITableViewCell *cell;
    if ([key isEqual:PhotoRatingKey]) {
        cell = [self cellForRating:value indexPath:indexPath];
    } else {
        cell = [self detailCellForKey:key value:value];
    }
    return cell;
}
 
- (RatingCell *)cellForRating:(NSNumber *)rating
                    indexPath:(NSIndexPath *)indexPath
{
    // ...
}
 
- (UITableViewCell *)detailCellForKey:(NSString *)key
                                value:(id)value
{
    // ...
}

table view 的编辑

table view提供了简单易用的编辑功能,可以重新排序和删除cell。在发生这些事件的情况下,表数据源会通过代理方法的形式获得通知。因此,我们经常看到逻辑的代理方法来执行实际修改数据。

处理数据完全就是模型层的工作。模型层应该提供我们可以从数据源代理方法中调用的用来删除和重新排列数据的接口。用这种方法,控制器就只扮演了视图和模型层之间的协调者,不必知道模型层的实现细节。另外一个好处是,逻辑模型变得更容易的进行测试,因为它不再和控制器层的东西进行交互任务。

 

结论

Table view controllers (和其他控制器对象)大部分情况下都应该起着模型和视图对象之间的协调中介作用,他们不应该关心模型或者视图的具体实现细节。如果你记住这点,那么代表和数据源方法就变得更简单和更容易维护的样板代码了。

这样不仅会降低了Table view controllers 的代码规模和复杂性,而且使模型逻辑代码和视图代码放在了更合适的地方。控制器上下之间的实现细节都被隐藏在简单的API中,最终使得代码更容易理解和协同工作。

 


原文链接: Florian Kugler   翻译: 伯乐在线 christian
译文链接: http://blog.jobbole.com/53123/
转载必须在正文中标注并保留原文链接、译文链接和译者等信息。]

posted @ 2014-04-21 22:41  Jason zheng  阅读(245)  评论(0编辑  收藏  举报