干净的table view 代码

原文链接:http://www.objc.io/issue-1/table-views.html

Table views是iOS应用中最常用的模块。因此,很多代码都间接或者直接地跟table view有关,像数据的供应,table view的更新,行为的控制,对于选中操作的反应,上面只是举得几个例子。在这篇文章中,我们将要展示让代码更简洁并且更好结构的技术。


UITableViewController VS UIViewControler


苹果提供了UITableViewController作为table view的专门的view controller类。Table view controllers提供了一些费用方便并且实用的功能,从而可以帮助你避免不断地写样板一样的重复的代码。另一方面,table view controllers限制只能管理一个table view,并且这个table view是全屏展示的。然而,在大部分情况下,这就是你所需要的。如果不是,我们下面也会介绍很多方法来解决这个问题。


Table View 控制器的特点

Table View 控制器可以在第一次展示table view的时候帮助加载数据。更具体的说,可以帮助切换table view的编辑模式,可以对键盘通知做出响应,以及另外一些小的任务比如滚动条的刷新和清除table view cell的选中效果等等。为了能让这些特征正常工作,必须在子类中会重写的方法(比如viewWillAppear:和viewDidappear)中调用父类的相应方法。

Table View控制器对于一个标准的视图控制器还有一个唯一卖点:支持苹果的“拉动刷新”.。在table view中使用UIRefreshControl是唯一一个文档化的使用UIRefreshControl的方法。也有一些其他方法可以在别的情况下使用UIRefreshControl,但是这样很可能在下个iOS更新中就不能工作了。

上述这些提供了大部分苹果定义好的table view的标准接口。如果你的应用刚好符合这些标准的话,你可以使用table view控制器,这样可以避免写一些样板代码。

Table View控制器的限制

table view控制器的视图属性总是被设置成table视图。如果你想在table视图边上显示一些其他的东西你就没那么幸运了。除非你采用很奇怪的黑客方式。

如果你已经用代码或者xib文件的方式定义了一些接口,这样很容易就可以将一个table view控制器换成一个普通的视图控制器。如果你用的是storyboard,那这个过程就要费很多的步骤了。在storyboard中,如果不直接创建的话,不能直接将一个table view控制器转成一个普通的视图控制器。这意味着你要将所有的代码都复制到的新的视图控制器然后将原来的table view控制器删掉。

最后,你要将在这个转换过程中遗失的table view控制器一些功能重新添加进来。大部分的这些功能像是viewWillAppear和viewDidAppear这样的单行语句。编辑状态的开关需要实现一个行为方法来改变table view的编辑属性。最大的工作取决于重做对键盘的支持。

在你走这条路之前,还有另一个选择。

子类化视图控制器

除了将整个table view控制器移除之外,你还可以将它作为一个子视图添加到另一个视图控制器中。然后table view控制器依然只管理者table view,而父视图控制器依然可以管理其他你需要的方法。

- (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控制器定义一个代理协议,可以在父视图中实现这个协议。

@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
就像你看到的,这个结构的代价就是视图控制器之间的通讯消费,作为回报的是关注点分离和更好的重用。根据具体的使用场景,这可以使事情变得比之前更简单或者更复杂,这就取决于你的考虑和决定了。

关注点分离

在处理table view的时候,会涉及到很多模型、控制器和视图之间的边缘任务,为了将视图控制器和这些任务分离,我们尽可能将这些任务独立地放在更合适的地方。这样可以使代码更可读、可维护和可测试。

这里使用到的技术是在轻量级视图控制器描述的规则基础上的延伸和详细描述。请参考这篇文章重构我们的数据源和模型逻辑。在table view中,我们将更加关注视图控制器和视图之间的关注点分离。

将模型对象和cells之间的缝隙连接起来

在某些情况下,我们需要将我们要展示的数据传递到视图层。既然我们又想保持模型和视图之间的一个清晰的分界,我们就将这个输入的加载放到table view的数据源中了。

- (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的类别类里重构一下。

@implementation PhotoCell (ConfigureForPhoto)

- (void)configureForPhoto:(Photo *)photo
{
    self.photoTitleLabel.text = photo.name;
    NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
    self.photoDateLabel.text = date;
}

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

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier];
    [cell configureForPhoto:[self itemAtIndexPath:indexPath]];
    return cell;
}
示例代码中,初始化cell的时候时候block的方式,table视图的数据源已经分离到了一个单独的控制对象中,在这个例子中,这个block就像下面这样

TableViewCellConfigureBlock block = ^(PhotoCell *cell, Photo *photo) {
    [cell configureForPhoto:photo];
};
使cell可重用

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

在cell中处理cell的状态

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

- (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中。

@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的方法。

- (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的数据源会通过代理方法的形式获得通知。因此,我们经常看到我们在数据源的方法里处理数据。

处理数据完全就是模型层的工作。模型层应该提供我们可以从数据源代理方法中调用的用来删除和重排数据的接口。用这种方法,控制器就只扮演了view和模型层之间的协调者,而根本不需要知道模型层是如何实现的。还有另外的一个好处,模型层也能更好的进行测试,因为它不再跟控制器层的东西进行交叉。

结论

table view控制器(包括一般的控制器)大部分情况下都应该扮演模型和视图对象之间的中间者,他们不应该知道模型或者视图的具体实现细节。如果你将这个记载心里,那么代理和数据源方法就变得更小和更容易维护的样板代码了。

这样不仅会减少table view 控制器的代码大小和复杂度,这也使模型逻辑代码和视图代码在他们应该在的地方。控制器之上和之下的实现细节都被隐藏在简单的接口中了,这也使得代码更容易阅读以及更好的进行分工合作。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值