一种组件化的 Table View 的实现

作者: rickytan | 来源:发表于2017-11-29 15:39 被阅读24次

    背景

    最近在做一个项目时,需要实现一些列表界面,总体上是上下滚动的,中间的部分段有可以横滚的,有一个个小标签式的,也有可循环滚动的焦点图的……且类似的界面大量出现,并随机组合。可以参照网易云音乐,早期版本的蘑菇街,小红书等等。

    按以往的想法是,继承 UITableViewController 然后分多个 section,所有的数据与点击都在一个 VC 中完成。如果全是占满行的 Cell,勉强可以接受,但很快你会发现,你的代码变得庞大而臃肿,且不可维护。更要命的是,代码无法复用,且一旦有需求变动,留下 Bug 的几率很大。网上关于 UITableView 瘦身的优化方法已经很多了,基本上也就是增加一个 ViewModel 层,将代码换了个地方,没什么很大的意思。但是在这次遇到的项目背景下,我想应该可以用更好的组件化的方式来实现。

    组件定义

    我们定义 UITableView 中的一个 section 为一个组件(component),它需要管理自己的标头(header)、行高、Cell 数量等:

    @protocol RTTableComponent <NSObject>
    @required
    
    - (NSString *)cellIdentifier;
    - (NSString *)headerIdentifier;
    
    - (NSInteger)numberOfItems;
    - (CGFloat)heightForComponentHeader;
    - (CGFloat)heightForComponentItemAtIndex:(NSUInteger)index;
    
    - (__kindof UITableViewCell *)cellForTableView:(UITableView *)tableView
                                       atIndexPath:(NSIndexPath *)indexPath;
    - (__kindof UIView *)headerForTableView:(UITableView *)tableView;
    
    - (void)reloadDataWithTableView:(UITableView *)tableView
                          inSection:(NSInteger)section;
    - (void)registerWithTableView:(UITableView *)tableView;
    @optional
    
    - (void)willDisplayHeader:(__kindof UIView *)header;
    - (void)willDisplayCell:(__kindof UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath;
    
    - (void)didSelectItemAtIndex:(NSUInteger)index;
    
    @end
    

    上面代码中:- (void)registerWithTableView:(UITableView *)tableView 提供了一个入口供组件注册自定义的 UITableViewCell

    继承自 UIViewController——这里不用 UITableViewController 是为了灵活性,比如有时候 TableView 不需要占满屏——实现一个 RTComponentController,它维护一个成员为 id<RTTableComponent> 类型的数组:

    @interface RTComponentController : UIViewController <UITableViewDataSource, UITableViewDelegate>
    @property (nonatomic, readonly, strong) UITableView *tableView;
    @property (nonatomic, strong) NSArray <id<RTTableComponent> > *components;
    - (CGRect)tableViewRectForBounds:(CGRect)bounds;
    @end
    

    然后在具体的实现中,将大部分 DatasourceDelegate 的方法转发到 components 上:

    
    - (CGRect)tableViewRectForBounds:(CGRect)bounds
    {
        return bounds;
    }
    
    #pragma mark - UITableView Datasource & Delegate
    
    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
        return self.components.count;
    }
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        return self.components[section].numberOfItems;
    }
    
    - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
    {
        return self.components[section].heightForComponentHeader;
    }
    
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        return [self.components[indexPath.section] heightForComponentItemAtIndex:indexPath.row];
    }
    
    - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
    {
        return [self.components[section] headerForTableView:tableView];
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView
             cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        return [self.components[indexPath.section] cellForTableView:tableView atIndexPath:indexPath];
    }
    
    - (void)tableView:(UITableView *)tableView
    willDisplayHeaderView:(UIView *)view
           forSection:(NSInteger)section
    {
        if ([self.components[section] respondsToSelector:@selector(willDisplayHeader:)]) {
            [self.components[section] willDisplayHeader:view];
        }
    }
    
    - (void)tableView:(UITableView *)tableView
      willDisplayCell:(UITableViewCell *)cell
    forRowAtIndexPath:(NSIndexPath *)indexPath
    {
        if ([self.components[indexPath.section] respondsToSelector:@selector(willDisplayCell:forIndexPath:)]) {
            [self.components[indexPath.section] willDisplayCell:cell
                                                   forIndexPath:indexPath];
        }
    }
    
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
        if ([self.components[indexPath.section] respondsToSelector:@selector(didSelectItemAtIndex:)]) {
            [self.components[indexPath.section] didSelectItemAtIndex:indexPath.row];
        }
    }
    

    给定一个基础实现 RTBaseComponent,没有标头,0 个 Cell:

    @interface RTBaseComponent : NSObject <RTTableComponent>
    @property (nonatomic, weak) id<RTTableComponentDelegate> delegate;
    
    @property (nonatomic, strong) NSString *cellIdentifier;
    @property (nonatomic, strong) NSString *headerIdentifier;
    
    + (instancetype)componentWithTableView:(UITableView *)tableView;
    + (instancetype)componentWithTableView:(UITableView *)tableView delegate:(id<RTTableComponentDelegate>)delegate;
    
    - (instancetype)init NS_UNAVAILABLE;
    - (instancetype)initWithTableView:(UITableView *)tableView;
    - (instancetype)initWithTableView:(UITableView *)tableView delegate:(id<RTTableComponentDelegate>)delegate NS_DESIGNATED_INITIALIZER;
    
    - (void)registerWithTableView:(UITableView *)tableView NS_REQUIRES_SUPER;
    - (void)setNeedUpdateHeightForSection:(NSInteger)section;
    
    @end
    
    
    @interface RTBaseComponent ()
    @property (nonatomic, weak) UITableView *tableView;
    @end
    
    
    @implementation RTBaseComponent
    
    - (instancetype)initWithTableView:(UITableView *)tableView delegate:(id<RTTableComponentDelegate>)delegate
    {
        self = [super init];
        if (self) {
            self.cellIdentifier = [NSString stringWithFormat:@"%@-Cell", NSStringFromClass(self.class)];
            self.headerIdentifier = [NSString stringWithFormat:@"%@-Header", NSStringFromClass(self.class)];
            self.tableView = tableView;
            self.delegate = delegate;
    
            [self registerWithTableView:tableView];
        }
        return self;
    }
    
    - (void)registerWithTableView:(UITableView *)tableView
    {
        [tableView registerClass:[UITableViewCell class]
          forCellReuseIdentifier:self.cellIdentifier];
    }
    
    - (NSInteger)numberOfItems
    {
        return 0;
    }
    
    - (CGFloat)heightForComponentHeader
    {
        return 0.f;
    }
    
    - (CGFloat)heightForComponentItemAtIndex:(NSUInteger)index
    {
        return 0.f;
    }
    
    ......
    
    @end
    

    然后继承自 RTBaseComponent,实现一个有标头的组件:

    @interface RTHeaderComponent : RTBaseComponent
    @property (nonatomic, copy) NSString *title;
    @property (nonatomic, strong) UIFont *titleFont;
    @property (nonatomic, strong) UIColor *titleColor;
    @property (nonatomic, strong) UIView *accessoryView;
    
    - (CGRect)accessoryRectForBounds:(CGRect)bounds;
    
    @end
    
    @implementation RTHeaderComponent
    
    - (void)registerWithTableView:(UITableView *)tableView
    {
        [super registerWithTableView:tableView];
        [tableView registerClass:[UITableViewHeaderFooterView class]
    forHeaderFooterViewReuseIdentifier:self.headerIdentifier];
    }
    
    - (CGFloat)heightForComponentHeader
    {
        return 36.f;
    }
    
    - (__kindof UIView *)headerForTableView:(UITableView *)tableView
    {
        UITableViewHeaderFooterView *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:self.headerIdentifier];
        header.textLabel.text = self.title;
        header.textLabel.textColor = self.titleColor ?: [UIColor darkGrayColor];
        self.accessoryView.frame = [self accessoryRectForBounds:header.bounds];
        [header.contentView addSubview:self.accessoryView];
        return header;
    }
    
    - (void)willDisplayHeader:(__kindof UIView *)header
    {
        UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)header;
        headerView.textLabel.font = self.titleFont ?: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
        self.accessoryView.frame = [self accessoryRectForBounds:header.bounds];
    }
    
    ......
    
    

    注意,上面需要在 willDisplayHeader: 中设置 textLabel 的字体(可能是苹果的 bug)

    同时为了满足横滚等需求,实现一个 RTCollectionComponent,它管理一个 UICollectionView 实例,实现它的 DatasourceDelegate,提供一个入口供子类注册自定义的 UICollectionViewCell,并最终将它添加到 cell.contentView 上:

    @interface RTCollectionComponent : RTActionHeaderComponent <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout>
    @property (nonatomic, readonly, strong) UICollectionView *collectionView;
    
    - (void)configureCollectionView:(UICollectionView *)collectionView NS_REQUIRES_SUPER;
    
    - (CGRect)collectionViewRectForBounds:(CGRect)bounds;
    
    @end
    

    结果

    在 Demo 中项目自定义了四种 Component

    • RTDemoTagsComponent
    • RTDemoBannerComponent
    • RTDemoImageItemComponent
    • RTDemoItemComponent

    最终实现的界面效果类似如下:


    19-5.png 19-6.png

    而整个 VC 的代码只是挂载了四个 Component,在其他 VC 中这些组件也可以选择性地复用,且有较高的配置灵活性:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        RTDemoTagsComponent *tags = [RTDemoTagsComponent componentWithTableView:self.tableView
                                                                       delegate:self];
        self.components = @[tags,
                            [RTDemoImageItemComponent componentWithTableView:self.tableView
                                                                    delegate:self],
                            [RTDemoBannerComponent componentWithTableView:self.tableView
                                                                 delegate:self],
                            [RTDemoImageItemComponent componentWithTableView:self.tableView
                                                                    delegate:self],
                            [RTDemoItemComponent componentWithTableView:self.tableView
                                                               delegate:self]];
    
        [tags reloadDataWithTableView:self.tableView
                            inSection:0];
    }
    

    单个 Component 的数据可以由 VC 发起请求后一起塞回,或者每个 Component 自己在 - (void)reloadDataWithTableView:inSection: 方法中请求,而 VC 负责触发一次请求,取决于具体实现与需求。

    总结

    一个程序员的日常无非就是在处理产品经理的各种合理非理的需求,在真正动手之前多停下来思考一下,磨刀不误砍柴功,以不变应对万变的需求。在上面这种实现中,无论临时增加或减少一个展示段,无非就是增加、减少一个 Component,修改起来没有痛苦。而如果像以前一样用 switch (indexPath.section) 的办法,不仅改起来不方便,还容易 Crash

    以上所有代码匀可以在 Github 上找到,并已经发布到 Cocoapods

    本文只针对 UITableView 做了简单的组件化,同样的操作可以应用到 UICollectionView 上,且更多实用,并且现在已经有开源实现:https://github.com/Instagram/IGListKit,或者 DDComponent,使用更简单。如何更全面的、完全的组件化?参考以下两个实现:HubFrameworkComponentKit

    相关文章

      网友评论

        本文标题:一种组件化的 Table View 的实现

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