阅读 2201

如何写好静态的TableView

前言

可能之前的表述不是特别明确,我的方案不是静态页面的通用实现。我的方案针对的是类似一些设置界面之类的简单的静态的tableView。看到有很多人认为这样的方案感觉实现起来会变麻烦,这个可能就是思考问题的侧重点不同。我思考的侧重点是后期的维护修改、应对频繁的需求变更 欢迎交流~~

静态的tableView类似设置界面、个人主页等等几乎是每个APP都会涉及到的一个模块。我相信大家都有一些自己的套路来如何处理这类界面。写这篇文章的目的是抛砖引玉想要和大家来交流交流。

一些常见的写法

从具体的写法来切入,以下是我能想到的一些写法。

1. 山顶洞人写法

啥都不封装

    if (indexPath.section == 0) {
        if (indexPath.row == 0) {
            
        }else if (indexPath.row == 1) {
            
        }
    }else if (indexPath.section == 1) {
        if (indexPath.row == 0) {
            
        }else if (indexPath.row == 1) {
            
        }
    }
复制代码

各种嵌套if判断indexPath.section indexPath.row拿到对应的cell显示或者跳转。这种方式可读性差,不好扩展应该没人这么写了吧,可能你刚学iOS开发的时候这么写过。

2. 纯代码 + 枚举

用一条枚举来对应的一条cell。 数据源用枚举数组,或者也可以用带有枚举属性的对象数组。

    self.dataArray = @[@[@(settingTypeAccount)],
                       @[@(settingTypeMessage),@(settingTypePrivacy)],
                       @[@(settingTypeHelp),@(settingTypeAboutUs)]];
复制代码

代理方法里可以拿到枚举直接用switch判断

    switch (type) {
        case settingTypeHelp:
            break;
            .
            .
            .
        default:
            break;
    }
复制代码

用switch来判断具体具体的cell,首先可读性相较于if判断高了不少,并且在增加删除cell的情况下,xcode会有提示来帮助不至于漏掉一些地方。这种方式会有比较多的重复代码,而且在添加、删除、调整cell的时候不够高效。

3. storyboard的静态cell

首先storyboard方式相较于纯代码,不用跑起来就能看见界面,相对比较直观。而且在开发速度方面也有不小的优势。但是我从自身开发过程中的情况看来,这种方式在需求频繁变更的情况下还是比较蛋疼的。

4. 加一层中间层

没有什么封装是加一层中间层解决不了的,如果有那么再加一层 -- 鲁迅

😁开个玩笑,来看看加一层怎么样操作。首先我认为要有一个概念,封装在一定程度上是不会减少代码量的。该写的代码你还是要写的,只是合理的结构可以让代码可读性更好,可扩展性也更好。

@interface tableModel : NSObject
- (void)addASection:(tableSectionModel *)section;
@property (nonatomic,strong) NSMutableArray <tableSectionModel *> *sections;
.
.
.
@end

@interface tableSectionModel : NSObject
- (void)addARow:(tableRowModel *)row;
@property (nonatomic,strong) NSMutableArray <tableRowModel *> *rows;
.
.
.
@end

@interface tableRowModel : NSObject
@property (nonatomic,assign) NSInteger rowHeight;
.
.
.
@end
复制代码

我们一开始就已近知道了table是如何展示的,包括cell的显示顺序,cell的显示样式、行高等等。那么我们可以把能够描述一个cell的所有的数据都抽象成一个rowModel的数据,然后把能描述每个section的的所有数据抽象成一个sectionModel的数据。那么我们只需要生成对应的sectionModel的数组就可以来描述一个table了,然后我们在数据源里解析model里面的数据完成显示。这种方式已经相对比较合理了,但是内部还是有比较大的封装余地。

show you my code

我看过挺多的类似三行代码实现设置界面的方案,基本都是上面第四种方法的进一步封装,内部实现了几种常见的cell样式,用一个枚举来对应具体的样式,然后给每个每个rowModel添加一个cell样式的属性。这样一来通过简单的设置rowModel的cell样式就能拿到具体的cell。当然这样的方式已经能够应对大部分的情况,并且写起界面来也是很爽了。但是我还是有几个地方不是太满意,需要去尝试解决这些不满意。

1. 添加section和row的方式

我希望添加section和row的时候这部分的代码是一个整体,单纯- (void)addASection:(tableSectionModel *)section;这样的写法在我看来还不够整体,因为你没办法保证这部分的代码一定是写在了一个地方。思来想去,我想到了Masonry的写法。

[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
    // do something        
}];
复制代码

这种写法解决了我不满意的地方,所以最后我希望的写法是

    [self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
        [section zhn_addRow:^(ZHNStaticTableRow *row) {
            
        }];
        
        .
        .
        .
    }];
复制代码

2.dataSource delegate 重复代码的问题

我们清楚dataSourcedelegate是一对一的,所以代理方法和数据源方法肯定是只能写在一个地方。你可能会说那么我们再加一层Manger来管理sectionModel的数组,然后把tableView的数据源和代理设置为manager,然后在manager内部实现dataSourcedelegate解析sectionModel的数组展示界面。这样一来我们只需要配置sectionModel就可以了。但是这样做那么万一我们在控制器上想要监听tableView的滑动呢?思来想去,我最后尝试用消息转发 + 断言的方式来尝试解决这个问题。

消息转发实现代理一对多

代理是一对一的,通知是一对多的。

刚开始学iOS的时候,我们肯定都听过这样一句话,来描述代理和通知的不同。但是其实通过消息转发,我们也是可以来实现代理的一对多的。

如果对消息转发没啥概念的可以看看这篇博客。简单理解就是当调用方法的时候,系统通过isa指针层层查找方法列表,找不到方法的时候,在报找不到方法之前,系统还额外提供了几个方法提供给我们去实现这个方法。

代理一对多的主要实现的逻辑:

  • 1.提供一个delegate容器,存放代理。

  • 2.- (BOOL)respondsToSelector:(SEL)aSelector判断代理容器里的代理如果实现了代理方法,这个方法需要返回YES。如果返回NO,系统就判定没有实现代理方法,那么就不会调用方法,那么也就不会有后面的一系列的流程了。

    1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector返回方法的签名。不返回签名后面的消息转发方法也不会调用。
    1. - (void)forwardInvocation:(NSInvocation *)anInvocation方法里遍历delegate容器,转发方法。

断言

断言 (assertion) 在 Cocoa 开发里一般用来在检查输入参数是否满足一定条件,并对其进行“论断”。这是一个编码世界中的哲学问题,我们代码的使用者 (有可能是别的程序员,也有可能是未来的自己) 很难做到在不知道实现细节的情况下去对自己的输入进行限制。大多数时候编译器可以帮助我们进行输入类型的检查,但是如果代码需要在特定的输入条件下才能正确运行的话,这种更细致的条件就难以控制了。在超过边界条件的输入的情况下,我们的代码可能无法正确工作,这就需要我们在代码实现中进行一些额外工作。

上面这段介绍是从喵神一篇断言tips里的摘抄。在很多的第三方库中你肯定也见过类似比如AFNetworking中随便一搜NSAssert(NO, @"State method should never be called in the actual dummy class");类似的断言非常常见。简单理解当我们输入一个不合法参数的情况的时候,程序就直接崩溃了,并且打印了断言里的描述。那么我们一眼就能知道我们的输入出问题了,并且问题出在哪里。

这里我们为什么用断言,由于我内部实现了某些数据源和代理。那么我们肯定不希望外部再实现这些实现过的方法。那么我们肯定需要做一些约束,这里用断言显然是最合适的。如果外部实现了实现过的方法,直接崩溃并且打印提示信息。

3. 方便切换

类似三行代码实现设置界面的方案内部定义几种样式的方案,如果我们想要把我们已经写好的界面切换到这种方案下,代价相对还是比较大的。我们项目中肯定也实现了一些cell,我希望我之前的cell能无缝的接入进去。针对这种情况我在rowModel里添加了一个cellClass属性来指定cell,和一个displayCellHandle block来设置cell的一些样式。

瞄一眼写法

    [self.tableView zhn_initializeEnvironmentWithDefaultRowHeight:44
                                                 defaultCellClass:[NormalSettingTableViewCell class]
                                             defaultSectionHeader:nil
                                              defaultHeaderHeight:20
                                             defaultSectionFooter:nil
                                              defaultFooterHeight:0
                                                 originalDelegate:self
                                               originalDatasource:self];
    
    [self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
        [section zhn_addRow:^(ZHNStaticTableRow *row) {
            row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
                cell.textLabel.text = @"账号与安全";
            };
            row.selectCellHandle = ^(UITableView *tableView, NSIndexPath *indexPath) {
                NSLog(@"账户与安全");
            };
        }];
    }];
    
    [self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
        [section zhn_addRow:^(ZHNStaticTableRow *row) {
            row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
                cell.textLabel.text = @"新消息通知";
            };
        }];
        [section zhn_addRow:^(ZHNStaticTableRow *row) {
            row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
                cell.textLabel.text = @"隐私";
            };
        }];
        [section zhn_addRow:^(ZHNStaticTableRow *row) {
            row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
                cell.textLabel.text = @"通用";
            };
        }];
    }];
复制代码

总结

代码在这里 github.com/zhnnnnn/ZHN…

这是我的方案,还没来得及在实际的项目中使用过。抛砖引玉,希望大家能够不吝赐教。我也很想知道大型知名项目里大家都是怎么写这部分代码的。

关注下面的标签,发现更多相似文章
评论