美文网首页
TableView之协议托管

TableView之协议托管

作者: Joyous | 来源:发表于2018-03-02 12:25 被阅读13次

    auu.space

    TableView之协议托管

    一直以来,UITableView在我们的程序开发中都扮演着一个重要的角色,随着开发者的日益增多和需求的不断变化,就出现了各种各样的写法,今天我们来说一种很早以前就出现的方式,可以叫做协议托管(我是没见过别处专业的叫法,暂且如是称呼吧)。
    其实这种模式早期是在Facebook开源的Three20就有了,不过这个库最后不再更新;随后这个开发团队的一个开发者做了又一套类似的库Nimbus,文档很全,用着也很方便,但是更新了一段时间以后又停止了维护。当然了,这两个库也是包含了很多的内容,今天所说的也只是其中的一部分。
    现在由于swift的大热,又出现一个很强势的库Eureka,有兴趣的也可以去看一看。

    一、协议托管

    看了上篇文章或者没看的,都知道,对于一个UITableView来说,我们主要的时间都是花在其两个协议上,而且由于苹果对协议方法的分散,让我们在多个地方做了过多重复的工作,那么,第一步我们就来接管这两个协议:

    1. 新建一个类AUUTableModel来接管UITableViewDataSource,用来处理TableView的数据源。
    2. 新建一个类AUUTableAction来接管UITableViewDelegate,用来处理TableView的方法回调、事件控制等等。

    此时我们的TableView可以这样写了:

    - (UITableView *)tableView {
        if (!_tableView) {
            _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
            _tableView.dataSource = self.tableModel;
            _tableView.delegate = self.tableAction;
        }
        return _tableView;
    }
    

    tableModeltableAction里接收的都是一些协议方法,因为几乎所有的方法里都带有UITableView的参数,所以,他们的关系如下图:

    image

    二、定制UITableViewCell

    我们这里主要讲的就是接管整个tableView的定制和处理,减少外部不断判断所出现的代码浪费,那么我们就需要一个类来管理每个cell对应的数据,这里我们来用一个协议来做限定,让使用者可以自由定制:

    /**
     Cell对应的Object需要实现的方法
     */
    @protocol AUUCellObject <NSObject>
    
    @required
    
    /**
     当前object对应的cell的class
     */
    - (Class)cellClass;
    
    /**
     是否是可复用的
     */
    - (BOOL)reuseable;
    
    @end
    

    现在我们接管每个cell的数据源,但是我们还并不知道每个cell到底长啥样,那么我们再来用一个协议限定cell的一些属性:

    /**
     UITableViewCell 需要实现的协议方法
     */
    @protocol AUUCell
    
    @required
    
    /**
     当前cell的高度
     */
    + (CGFloat)heightWithObject:(id <AUUCellObject>)object;
    
    /**
     每次显示一个cell的时候都调用的方法,用于刷新cell上的数据
     */
    - (BOOL)shouldUpdateWithObject:(id <AUUCellObject>)object;
    
    @end
    

    三、数据展示

    现在TableView的协议我们也接管过来了,cell和数据我们也先定好了,就可以来显示数据了。
    对于一个tableView是有多个分组的概念的(虽然我们有时也能看到只有一个分组的情况,但是这不也是分组么),在分组下管理的是这个分组的各个cell,但是这里我们管理的是每个分组下的数据信息,下面我们就来定义一个分组的模型(暂时就设置这么简单的一个属性):

    @interface AUUTableSection : NSObject
    
    /**
     此分组下的cell object,就是每个cell所对应的数据信息
     */
    @property (nonatomic, strong) NSMutableArray <id <AUUCellObject>> *rows;
    
    @end
    

    然后在定义一个属性来存所有的分组:

    /**
     table中的分组
     */
    @property (nonatomic, strong) NSMutableArray *sections;
    

    数据现在有了,那么就可以开始以前的一项重复的工作来显示各条数据了,但是现在我们只需要在tableModel里写一次即可:

    // 设置分组的个数
    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
        return self.sections.count;
    }
    
    // 设置每个分组下的行数
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
        return [[[self.sections objectAtIndex:section] rows] count];
    }
    
    // 设置每行的cell视图
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        // 获取当前tableView的indexPath这个位置对应的数据信息
        id <AUUCellObject> object = [self objectForTable:tableView atIndexPath:indexPath];
        
        // 拼接复用的标识,如果是可复用的cell,直接用cell的类名来当做标识
        NSString *identifier = NSStringFromClass(object.cellClass);
        if (!object.reuseable) {
            // 如果不可复用,就拼接上数据的hash值,来控制唯一性
            identifier = [identifier stringByAppendingFormat:@"%@", @([(NSObject *)object hash])];
        }
        
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
        if (!cell) {
            // 根据数据信息对应的cell类型创建实例
            cell = [[object.cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
        }
        
        // 更新cell数据
        if ([cell conformsToProtocol:@protocol(AUUCell)] && [cell respondsToSelector:@selector(shouldUpdateWithObject:)]) {
            [(id <AUUCell>)cell shouldUpdateWithObject:object];
        }
        
        return cell;
    }
    

    四、高度控制

    cell的视图我们创建好了,但是高度怎么控制呢?高度设置是在tableViewdelegate里就是现在的tableAction,但是数据都在dataSource里就是现在的tableModel,看看上面画的关系图,我们可以定义一个协议来要求tableModel给我指定的数据:

    /**
     AUUTableModel需要实现的方法
     */
    @protocol AUUCellFactory
    
    @required
    
    /**
     获取指定位置的object
    
     @param tableView 当前的tableView
     @param indexPath 当前的索引
     @return object
     */
    - (id <AUUCellObject>)objectForTable:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath;
    
    @end
    

    拿到了数据信息,那么高度的话就好说了:

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        if ([tableView.dataSource conformsToProtocol:@protocol(AUUCellFactory)] &&
            [tableView.dataSource respondsToSelector:@selector(objectForTable:atIndexPath:)]) {
            // 拿到当前位置的数据信息
            id <AUUCellObject> object = [(id <AUUCellFactory>)tableView.dataSource objectForTable:tableView atIndexPath:indexPath];
            if ([object.cellClass respondsToSelector:@selector(heightWithObject:)]) {
                // 获取高度
                return [object.cellClass heightWithObject:object];
            }
        }
        return 44.0;
    }
    

    五、测试一下

    实现部分的基类

    因为我们上面做的对于cell和数据的控制都是通过协议来做的,目的当然很明确,就是为了能够减少耦合,所以,为了在项目里写的方便,可以在实现的部分写一个基类来简化一些重复的工作,如:

    • BaseCell
    @interface AUUBaseCell : UITableViewCell <AUUCell>
    - (void)initialize;
    @end
    
    @implementation AUUBaseCell
    + (CGFloat)heightWithObject:(id<AUUCellObject>)object {
        return 44.0;
    }
    - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
        if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
            [self initialize];
        }
        return self;
    }
    - (void)initialize { }
    - (BOOL)shouldUpdateWithObject:(id<AUUCellObject>)object {
        return YES;
    }
    @end
    
    
    • BaseObject
    @interface AUUBaseObject : NSObject <AUUCellObject>
    + (instancetype)objectWithClass:(Class)cls;
    + (instancetype)object;
    @end
    
    @interface AUUBaseObject()
    @property (nonatomic) Class cls;
    @end
    @implementation AUUBaseObject
    + (instancetype)object {
        return nil;
    }
    + (instancetype)objectWithClass:(Class)cls {
        AUUBaseObject *object = [[self alloc] init];
        object.cls = cls;
        return object;
    }
    - (Class)cellClass {
        return self.cls;
    }
    - (BOOL)reuseable {
        return YES;
    }
    @end
    

    举例的cell

    下面在实现各个cell的时候,就可以如下写了:

    • .h
    @interface AUUTextCell : AUUBaseCell
    @end
    
    @interface AUUTextObject : AUUBaseObject
    + (instancetype)objectWithText:(NSString *)text;
    @end
    
    • .m
    @interface AUUTextObject ()
    @property (copy, nonatomic) NSString *text;
    @end
    
    @implementation AUUTextObject
    
    + (instancetype)objectWithText:(NSString *)text {
        AUUTextObject *object = [super objectWithClass:[AUUTextCell class]];
        object.text = text;
        return object;
    }
    
    @end
    
    @interface AUUTextCell()
    @property (nonatomic, strong) UILabel *label;
    @end
    
    @implementation AUUTextCell
    
    + (CGFloat)heightWithObject:(id<AUUCellObject>)object {
        return  44;
    }
    
    - (BOOL)shouldUpdateWithObject:(AUUTextObject *)object {
        self.label.text = object.text;
        return YES;
    }
    
    - (void)initialize {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, [UIScreen mainScreen].bounds.size.width - 40, 44)];
        self.label.font = [UIFont systemFontOfSize:14];
        self.label.textColor = [UIColor redColor];
        [self addSubview:self.label];
    }
    
    @end
    

    这里呢就先写这一个,更多的可以看一下测试代码。

    组装数据到页面

    在一开始的时候我们就创建了tableModel下面我们就来添加数据:

    [self.tableModel appendObject:[AUUTextObject objectWithText:@"title"]];
    [self.tableModel appendObject:[AUUInputObject object]];
    [self.tableModel appendObject:[AUUImageObject object]];
    [self.tableModel appendObject:[AUUImageScrollerObject object]];
    

    好了,我们添加了4个celltableView里,效果如下:

    image

    至此,一个tableView已经完美展示。

    六、事件响应

    上面的操作都只是出于看静态页面的状态,我们需要的是可以交互啊,就像最原始的写法一样:

    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    

    我们在里面收到tableView的调用以后就去做一些事情,但是现在我们的目的是接管这些协议方法,不给外面使用的机会。
    为了解决这个问题,就该强化我们的tableAction了。

    响应对应缓存

    我们来创建一个缓存类_AUUActionCache,用来缓存每一行设定的响应方法,里面包含如下几个属性:

    @property (weak, nonatomic) id target;
    @property (nonatomic) SEL sel;
    @property (copy, nonatomic) AUUCellSelectedAction action;
    

    提供外部注册事件的方法

    为了适应不同的需求,我们提供一下两种回调设置方式:

    • 单一对象的事件监听,只监听这一个数据对应的cell的点击事件
    // 通过block回调
    - (id<AUUCellObject>)attachObject:(id<AUUCellObject>)object action:(AUUCellSelectedAction)action {
        if (object) {
            [self.objectActions setObject:[_AUUActionCache cacheWithAction:action] forKey:[self keyForObject:object]];
        }
        return object;
    }
    
    // 通过方法调用来回调
    - (id <AUUCellObject>)attachObject:(id<AUUCellObject>)object target:(id)target action:(SEL)action {
        if (object) {
            [self.objectActions setObject:[_AUUActionCache cacheWithTarget:target selector:action] forKey:[self keyForObject:object]];
        }
        return object;
    }
    
    • 某一类对象的监听,监听这一类及其子类的点击事件
    // 通过block回调
    - (void)attachClass:(Class)cls action:(AUUCellSelectedAction)action {
        [self.classActions setObject:[_AUUActionCache cacheWithAction:action] forKey:(id <NSCopying>)cls];
    }
    
    // 通过方法调用来回调
    - (void)attachClass:(Class)cls target:(id)target action:(SEL)action {
        [self.classActions setObject:[_AUUActionCache cacheWithTarget:target selector:action] forKey:(id <NSCopying>)cls];
    }
    

    事件响应

    在上面我们添加了AUUCellFactory这么一个协议,那么数据肯定不在话下了,既然能拿到数据,那就能找到其对应的回调操作,如下:

    - (void)performActionForObject:(id <AUUCellObject>)object {
        if (!object) {
            return;
        }
        
        _AUUActionCache *action = [self.objectActions objectForKey:[self keyForObject:object]];
        if (!action) {
            Class keyClass = [(NSObject *)object class];
            
            for (Class class in self.classActions.allKeys) {
                if ([keyClass isKindOfClass:class] || [keyClass isSubclassOfClass:class]) {
                    // 需要做处理,比如  A<-B<-C
                    // 我添加了A、B的监听,当我拿C class来取响应者的时候,到底是取谁?
                    // 所以这里只是简单的写了一下
                    action = [self.classActions objectForKey:(id<NSCopying>)class];
                }
            }
        }
        
        if (!action) {
            return;
        }
        
        if (action.action) {
            action.action(object);
        }
        if (action.target && action.sel && [action.target respondsToSelector:action.sel]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [action.target performSelector:action.sel withObject:object];
    #pragma clang diagnostic pop
        }
    }
    

    七、再测试一下

    响应事件添加了,我们再来看一下实现,跟第一次测试也没多大的却别,跑起来试一下,在控制台就会出现想要的log结果了。

    [self.tableAction attachClass:[AUUTextObject class] action:^(AUUTextObject *object) {
        NSLog(@"tap : %@", object.text);
    }];
        
    [self.tableAction attachClass:[AUUImageObject class] target:self action:@selector(imageAction:)];
        
    [self.tableModel appendObject:[AUUTextObject objectWithText:@"title"]];
    [self.tableModel appendObject:[AUUInputObject object]];
    [self.tableModel appendObject:[self.tableAction attachObject:[AUUImageObject object] action:^(id<AUUCellObject> object) {
        NSLog(@"tap 你点击了这个图片cell");
    }]];
    [self.tableModel appendObject:[AUUImageScrollerObject objectWithDelegate:self]];
    

    八、更多的操作

    这里我们的测试例子,只是很简单的写了一下这个操作过程,不过对于一般不过于复杂的项目来说,貌似这就够用了啊。

    非要自己实现一些协议

    当然,tableView的协议方法有很多,就算自己再封装一个库也不会做到刚好符合每一个人的需求,而且对于苹果公司来说,随着技术的不断更新和进步,也肯定会有更多的API开放出来,于是,作为应用层的一次封装,提供给使用者更多的选择还是很有必要的。
    在这里,我推荐去看一看NimbusNITableAction下对于Forward Invocations的一些实现。

    Cell的自更新

    响应式设置,就像是集合了ReactiveCocoa的操作一样,每当有数据变动,都能及时的将变动后的结果展示出来,这里的这种写法当然也行啊,就看你怎么封装了。
    不过,我推荐去看个现成的内容Eureka,里面对于数据和cell的封装更紧密。

    九、总结

    这种方式把tableView协议里的操作进行了打散,重点的工作就是前期的tableModeltableAction的定制,在到应用的时候,是把各种类型的判断使用数据缓存来做了区分,这样在添加一个cell的时候就能省去更多的时间和判断逻辑。
    不信,可以增加一些增删的测试操作试试,如果用原始的写法来做的话,是不是有种要哭的感觉?

    测试代码

    相关文章

      网友评论

          本文标题:TableView之协议托管

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