优雅的书写 NSIndexPath

在当 UITableView 或者 UICollectionView 有多个 section,或者有多个静态的 row 时,想要在其中插入或删除某个 section 或 row,简直壮观。对此,本文将针对这一问题来讨论讨论。

环境信息
macOS 10.12.1
Xcode 8.2.1
iOS 10.2

在迭代公司项目时,遇到一个要在几年前的 UITableView 中插入一个新 section 的需求。当我打开那个文件的时候,一千多行且杂乱无章的代码就不说了,遍地的 section == 1/2/3 这样的判断简直就是 bug 的聚集地,防不胜防。

首先我们来看一下 section 需要判断的地方,至少都有以下方法:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

更别说还有 UITableViewDelegate 中的 heightsizedidSelected 等方法,甚至其他和业务相关的逻辑,例如跳到某个指定的 indexPath,或者点击判断等等。

如此之多的 section,没有任何一行注释标明 0、1、2、3 代表什么,对新需求或者平时维护,都是很大的挑战。

枚举

和 section 的 0、1、2、3 很像,默认的枚举值也是有序的,并且,枚举也有自己的名字,不用再用到 section 的地方都标上注释。所以,首先考虑的是枚举:

// 定义视频详情页面中,每个 section 的含义
typedef NS_ENUM(NSInteger, VideoDetailSectionIndex) {
    VideoDetailSectionIndexVideoInfo = 0,     	  ///< 视频信息 section
    VideoDetailSectionIndexProductList,           ///< 推荐购买链接 section
    VideoDetailSectionIndexRecommendVideo,        ///< 推荐视频 section
    VideoDetailSectionIndexPraiseComments,        ///< 最赞评论
    VideoDetailSectionIndexLatestComments,        ///< 最新评论
};

对应的 delegate 或者 dataSource 方法的实现就可以写为:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    switch (indexPath.section) {
        case VideoDetailSectionIndexVideoInfo:
            return 100;
        case VideoDetailSectionIndexProductList:
      	    // 如果有购买链接,则返回具体高度
            if (self.productList.count > 0) {
                return 20 * self.productList.count;
            // 如果没有购买链接,则返回 0
            } else {
                return 0;
            }
            ...
        default:
            break;
    }
}

这样以来,当需求需要在某个 section 下插入一栏的时候,直接在枚举中插入值,然后在对应的方法实现中,进行实现即可。尽可能的减少了直接出现下标、不清楚 section 含义的问题。

section 个数

但还有一个比较棘手的问题,即 UITableViewDataSource 中返回 section 个数的方法。当需要新增或者删除 section 时,对应 numberOfSectionsInTableView: 返回的 section 个数也应该变化,那么是否意味着,每新增一个 section,除了要改枚举值以外,还要记得改 numberOfSectionInTableView: 的返回值,并且,该方法返回值,还会直接出现数字。

如果是 Swift,我们可以直接给枚举一个方法,返回当前成员个数。但 Objective-C 其实也不赖,有一种很取巧的方式:

// 定义视频详情页面中,每个 section 的含义
typedef NS_ENUM(NSInteger, VideoDetailSectionIndex) {
    VideoDetailSectionIndexVideoInfo = 0,     	  ///< 视频信息 section
    VideoDetailSectionIndexProductList,           ///< 推荐购买链接 section
    VideoDetailSectionIndexRecommendVideo,        ///< 推荐视频 section
    VideoDetailSectionIndexPraiseComments,        ///< 最赞评论
    VideoDetailSectionIndexLatestComments,        ///< 最新评论
    VideoDetailSectionIndexCount		  ///< section 总个数
};

可以除了有含义的 section 定义以外,再加一个成员,而最后一个成员,刚好就是 section 的个数,所以,就可以优雅的返回 section 个数了:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return VideoDetailSectionIndexCount;
}

小结

这种实现方式当然也有利有弊:

  • 能尽可能少的出现具体 section 的下标
  • 能清晰的表达每个 section 的含义
  • 能轻松解决新增、删除 section 时,要手动移动 section 下标问题
  • 每次编辑 section,只需要改枚举
  • 遇到要删除某个 section 时,可以先删除枚举,然后通过编译报错来删除与其相关的逻辑

弊端:

  • 对于动态的 section 无效

《优雅的书写 NSIndexPath》有4个想法

  1. 对于这种情况,我这边一般都是考虑减少if switch 的书写;
    我这边处理是创建Model,来创建数据,利用block 或者标识来处理逻辑

    1. 也可以,但是利用 block 我不是很赞同,我觉得项目迭代过程中,判断逻辑应该越简单越好,复杂的对象模型会增加维护成本。

发表评论

电子邮件地址不会被公开。