background

Recently, when I was working on a project, I needed to implement some list interface. Generally, it was scrolling up and down. In the middle, there were sections that could be rolled, there were small tabs, and there were focus images that could be cycled… And similar interfaces appear in large numbers, and in random combinations. You can refer to netease Cloud Music, the early version of Mogujie, xiaohongshu and so on.

The old idea is that you inherit UITableViewController and then you divide it into sections, and all the data and clicks are done in one VC. If it’s full of cells, that’s fine, but soon you’ll find that your code is big, bloated, and unmaintainable. What’s more, the code is not reusable, and the chances of bugs are high if requirements change. There are a lot of optimizations on the web to slim down UITableView, which basically just add a ViewModel layer and change the code in a different place, which is not very interesting. But in the context of the project I encountered, I think it could have been done in a better componentized way.

Component definition

We define a section of a UITableView as a component that needs to manage its own headers, row height, Cell count, and so on:

@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
Copy the code

– (void)registerWithTableView:(UITableView *)tableView provides an entry for components to register custom UITableViewCell.

Inheriting from UIViewController — we don’t use UITableViewController here for flexibility, like sometimes TableView doesn’t have to fill up the screen — implementing an RTComponentController, It maintains an array of members of type ID

:

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

Then, in the concrete implementation, most of the Datasource and Delegate methods are forwarded to 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]; }}Copy the code

Given a base implementation, RTBaseComponent, with no header and 0 cells:

@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; }... @endCopy the code

Then inherit from RTBaseComponent and implement a component with a header:

@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]; }...Copy the code

Note that you need to set the textLabel font in willDisplayHeader: (probably an apple bug)

RTCollectionComponent manages an instance of UICollectionView, implements its Datasource and Delegate, Provide a portal for subclasses to register a custom UICollectionViewCell and eventually add it to 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
Copy the code

The results of

In the Demo, the project defines four components:

  • RTDemoTagsComponent
  • RTDemoBannerComponent
  • RTDemoImageItemComponent
  • RTDemoItemComponent

The resulting interface looks like the following:

The entire VC code only mounts four components, which can be selectively reused in other VC and have high configuration flexibility:

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

A single Component of the data can be initiated by the VC plug back together after the request, or every Component in the – (void) reloadDataWithTableView: inSection: Method, and the VC is responsible for triggering a request, depending on the implementation and requirements.

conclusion

The daily routine of a programmer is nothing more than to deal with the product manager’s various reasonable unreasonable needs, before really start to stop to think about it, do not mistakenly cut firewood work, in order to cope with the needs of the ten thousand changes. In the implementation above, adding or subtracting a display section is nothing more than adding or subtracting a Component without pain. If you use switch (indexPath. Section) as before, it is not only inconvenient to change, but also easy to Crash.

All of the above code can be found on Github and will be published to Cocoapods after it is cleaned up

This article only for UITableView to do a simple componentization, the same operation can be applied to UICollectionView, and more practical, and now has open source implementation: github.com/Instagram/I… . How to more comprehensive, complete componentization? Refer to the following two implementations: HubFramework, ComponentKit