美文网首页iOS从入门到放弃iOS
【译】让你的tableView代码整洁

【译】让你的tableView代码整洁

作者: Bertram | 来源:发表于2016-08-04 17:30 被阅读0次

    tableView 是 iOS 应用程序中非常通用的组件,许多代码和tableView都有直接或者间接的关系,比如数据提供、更新tableView、控制它的行为以及响应选择事件,在本文中,我们将介绍来保持tableView代码清洁和良好的结构。

    UITableViewController vs. UIViewController

    苹果提供了UITableViewController作为tableView的专用视图控制器类。UITableViewController实现了非常实用的功能,可以帮助我们避免一遍又一遍的重复的写相同的代码模板。另一方面,UITableViewController被限制为只能管理一个tableView,而且其实充满整个屏幕的,在很多情况下,这正是我们所需要的,如果不是的话,有办法解决这个问题,我们将在下面讲解

    UITableViewController的特点

    UITableViewController在第一次展示的时候会加载tableView的数据。更具体的,它会帮你切换tableView的编辑模式,响应键盘通知以及一些小的任务,比如闪现侧边的滑动提示条和清除选中时的背景色。为了让这些特性生效,当你在子类中覆写类似 viewWillAppear:或者 viewDidAppear:等事件方法时,需要调用 super 版本。

    UITableViewController相对于普通的UIViewController有一个优点,就是它支持Apple实现的“下拉刷新”功能,目前唯一使用UIRefreshControl的方式就是在UITableViewController中,虽然可以努力让它在其他地方工作(见此处),但是可能在iOS的下一个版本就不支持了。

    这些要素加一起,为我们提供了大部分 Apple 所定义的标准 table view 交互行为,如果你的应用恰好符合这些标准,那么直接使用 table view controllers 来避免写那些死板的代码是个很好的方法。

    UITableViewController的限制

    UITableViewController的view属性永远是一个tableView,如果你稍后决定在tableView旁边展示一些其他的视图(比如地图),如果不是依赖其他的黑科技,别的就没有办法了。

    如果你在代码中使用的是xib文件来定义一个界面,那么会很简单的迁移的标准的UIViewController。如果你用的storyboards,那么这个过程会涉及到几个步骤。除非重新创建,否则你并不能在 storyboards 中将 UITableViewController 改成一个标准的 UIViewController。这意味着你必须将所有内容拷贝到新的 view controller,然后再重新连接一遍。

    最后,你需要把迁移后丢失的 UITableViewController 的特性给补回来。大多数都是viewWillAppear:或viewDidAppear:中简单的一条语句。切换编辑模式需要实现一个 action 方法,用来切换 tableView 的editing属性。大多数工作来自重新创建对键盘的支持。

    在选择这条路之前,其实还有一个更轻松的选择,它可以通过分离我们需要关心的功能(关注点分离),让你获得额外的好处:
    使用Child View Controllers

    和完全抛弃 UITableViewController 不同,你还可以将它作为 child view controller 添加到其他 view controller 中(关于此话题的文章)。这样,parent view controller 在管理其他的你需要的新加的界面元素的同时,UITableViewController 还可以继续管理它的tableView。

    - (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];
    }
    

    如果你使用这个解决方案,你就必须在 child view controller 和 parent view controller 之间建立消息传递的渠道。比如,如果用户选择了一个 table view 中的 cell,parent view controller 需要知道这个事件来推入其他 view controller。根据使用习惯,通常最清晰的方式是为这个 table view controller 定义一个 delegate protocol,然后到 parent view controller 中去实现。

    @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
    

    就像你看到的那样,这种结构为 view controller 之间的消息传递带来了额外的开销,但是作为回报,代码封装和分离非常清晰,有更好的复用性。根据实际情况的不同,这既可能让事情变得更简单,也可能会更复杂,需要读者自行斟酌和决定。

    分离关注点(Separating Concerns)

    当处理 table views 的时候,有许多各种各样的任务,这些任务穿梭于 models,controllers 和 views 之间。为了避免让 view controllers 做所有的事,我们将尽可能地把这些任务划分到合适的地方,这样有利于阅读、维护和测试。
    这里描述的技术是文章更轻量的 View Controllers 中的概念的延伸,请参考这篇文章来理解如何重构 data source 和 model 的逻辑。结合 table views,我们来具体看看如何在 view controllers 和 views 之间分离关注点。

    搭建 Model 对象和 Cells 之间的桥梁

    有时我们需要将想显示的 model 层中的数据传到 view 层中去显示。由于我们同时也希望让 model 和 view 之间明确分离,所以通常把这个任务转移到 table view 的 data source 中去处理:

    - (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;
    }
    
    - (void)setSelected:(BOOL)selected animated:(BOOL)animated {
        [super setSelected:selected animated:animated];
        
        // Configure the view for the selected state
    }
    

    但是这样的代码会让 data source 变得混乱,因为它向 data source 暴露了 cell 的设计。最好分解出来,放到 cell 类的一个 category 中。

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

    有了上述代码后,我们的 data source 方法就变得简单了。

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier];
        [cell configureForPhoto:[self itemAtIndexPath:indexPath]];
        return cell;
    }
    

    在我们的示例代码中,table view 的 data source 已经分解到单独的类中了,它用一个设置 cell 的 block 来初始化。这时,这个 block 就变得这样简单了:

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

    让 Cells 可复用

    有时多种 model 对象需要用同一类型的 cell 来表示,这种情况下,我们可以进一步让 cell 可以复用。首先,我们给 cell 定义一个 protocol,需要用这个 cell 显示的对象必须遵循这个 protocol。然后简单修改 category 中的设置方法,让它可以接受遵循这个 protocol 的任何对象。这些简单的步骤让 cell 和任何特殊的 model 对象之间得以解耦,让它可适应不同的数据类型。

    在 Cell 内部控制 Cell 的状态

    如果你想自定义 table views 默认的高亮或选择行为,你可以实现两个 delegate 方法,把点击的 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;
    }
    

    然而,这两个 delegate 方法的实现又基于了 view controller 知晓 cell 实现的具体细节。如果我们想替换或重新设计 cell,我们必须改写 delegate 代码。View 的实现细节和 delegate 的实现交织在一起了。我们应该把这些细节移到 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 层和 controller 层的实现细节分离开。delegate 肯定得清楚一个 view 该显示什么状态,但是它不应该了解如何修改 view 结构或者给某些 subviews 设置某些属性以获得正确的状态。所有这些逻辑都应该封装到 view 内部,然后给外部提供一个简单的 API。

    控制多个 Cell 类型

    如果一个 table view 里面有多种类型的 cell,data source 方法很快就难以控制了。在我们示例程序中,photo details table 有两种不同类型的 cell:一种用于显示几个星,另一种用来显示一个键值对。为了划分处理不同 cell 类型的代码,data source 方法简单地通过判断 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 的 data source 通过 delegate 方法得到通知。因此,通常我们能在这些 delegate 方法中看到对数据的进行修改的操作。
    修改数据很明显是属于 model 层的任务。Model 应该为诸如删除或重新排序等操作暴露一个 API,然后我们可以在 data source 方法中调用它。这样,controller 就可以扮演 view 和 model 之间的协调者,而不需要知道 model 层的实现细节。并且还有额外的好处,model 的逻辑也变得更容易测试,因为它不再和 view controllers 的任务混杂在一起了。

    总结

    Table view controllers(以及其他的 controller 对象!)应该在 model 和 view 对象之间扮演协调者和调解者的角色。它不应该关心明显属于 view 层或 model 层的任务。你应该始终记住这点,这样 delegate 和 data source 方法会变得更小巧,最多包含一些简单的样板代码。
    这不仅减少了 table view controllers 那样的大小和复杂性,而且还把业务逻辑和 view 的逻辑放到了更合适的地方。Controller 层的里里外外的实现细节都被封装成了简单的 API,最终,它变得更加容易理解,也更利于团队协作。

    扩展阅读

    Table View Programming Guide
    Cocoa Core Competencies: Controller Object

    原文Clean table view code

    相关文章

      网友评论

        本文标题:【译】让你的tableView代码整洁

        本文链接:https://www.haomeiwen.com/subject/tmacsttx.html