background

In iOS development, UITableViewCell is definitely a very high frequency control. One of the architectures that we use a lot in actual development, which is apple’s official architecture, is the MVC architecture. So Model, View, and Controller correspond to M, V, and C. And UITableViewCell is part of the View. This article is mainly to talk about the reuse of UITableViewCell in real development, and there will be a little bit about Controller slimming later in the article.

In real development, our data sources are usually JSON data returned by the network interface or lists retrieved by local persistence (databases, etc.). We convert this data into models and use arrays to host the model data as data sources for UITableView. The UITableViewCell displays each item in the list, and most of the time, we will choose to load the data in the custom Cell, because the UITableViewCell will not fit the design draft in most cases.

Several ways to load cells

Let’s start by talking about how cells load data:

exposedCellFor data loading

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  	someModel = _dataSource[indexPath.row];
    TestCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
		cell.nameLabel = someModel.xxx;
    cell.titleLabel = someModel.xxx;
  	cell.dateLabel = someModel.xxx;
    return cell;
}
Copy the code

The advantages of the above approach are:

  • ModelViewIt’s decoupled. This is in line with AppleMVCArchitectural expectations.
  • CellIs reusable when another list needs the same thingCellTo show the differenceModelThe data can be reusedCell

There are drawbacks to this approach:

  • ifCellMore complex, possibly customCellIf there are more than five child controls, this method will have a lot of assignment code, which is not concise enough.
  • CellThere are too many child controls exposed in the declaration file.ControllerThere’s no need to know so much aboutCellThe details of.

Therefore, in real development, this approach is suitable for simple Cell structure data presentation.

Based on the complexity of the Cell, another way to load the data of the Cell is derived

Not to revealCellThe child controls are only exposedModel.

// cell.h #import <UIKit/UIKit.h> @class SomeModel; @interface TestCell : UITableViewCell @property (nonatomic, strong) SomeModel *model; @end // cell.m - (void)setModel:(SomeModel *)model { _model = model; self.leftLabel.text = model.xxx; self.rightLabel.text = model.xxx; self.titleLabel.text = model.xxx; . } // controller.m - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  { someModel = _dataSource[indexPath.row]; TestCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; cell.model = someModel; return cell; }Copy the code
  • The above way of loading data, inCellTo expose the model in the implementation file by rewritingsetterMethod to assign a value to data. After doing that, inControllerTo assign a value, you only need one line of code.

Of course, the above approach also has advantages and disadvantages.

Advantages are:

  • CellThe details are not found, and the child controls are well preservedCellIn the implementation file of.
  • ControllerIn theCellThe assignment code is only one line long.

The disadvantage is that:

  • ModelCellIt’s coupled to each other. That isModelViewIt’s coupled. It’s not very Apple friendlyMVCThought.
  • CellThe reusability is poor if the same style is encounteredCellDifferent business data, then we may need to define another one to belong to anotherModelProperty, which generates duplicate code during assignment.

The solution

In fact, the above two methods of Cell data loading can be used in development. In different situations, you can choose different methods. Personally, I think there is no problem. The problem we need to solve is the problem of poor Cell reuse and the coupling between Model and View when the same Cell loads different models.

Solution:

Our solution is protocol-oriented: define a protocol, make Model comply with the protocol and implement the methods in the protocol, obtain data in Cell through protocol-oriented and load data. You no longer need to define two Model properties in the Cell.

// TestCellConfigProtocol
#import <Foundation/Foundation.h>

@protocol TestCellConfigProtocol <NSObject>
- (NSString *)leftContent;
- (NSString *)rightContent;
@end

// Person.h
#import <Foundation/Foundation.h>
#import "TestCellConfigProtocol.h"

@interface Person : NSObject <TestCellConfigProtocol>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *job;

@end

// Person.m

#import "Person.h"

@implementation Person

- (NSString *)leftContent {
    return self.name;
}

- (NSString *)rightContent {
    return self.job;
}

@end
  
// User.h
#import <Foundation/Foundation.h>
#import "TestCellConfigProtocol.h"

@interface User : NSObject <TestCellConfigProtocol>
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, copy) NSString *hobby;
@end

// User.m
  
#import "User.h"

@implementation User

- (NSString *)leftContent {
    return self.nickName;
}

- (NSString *)rightContent {
    return self.hobby;
}

@end

// TestCell.h
#import <UIKit/UIKit.h>
#import "TestCellConfigProtocol.h"

@class TestViewModel;
@interface TestCell : UITableViewCell

@property (nonatomic, strong) id <TestCellConfigProtocol> model;

@end
  
// TestCell.m
  
#import "TestCell.h"

@interface TestCell ()
@property (weak, nonatomic) IBOutlet UILabel *leftLabel;
@property (weak, nonatomic) IBOutlet UILabel *rightLabel;
@end

@implementation TestCell

- (void)setModel:(id<TestCellConfigProtocol>)model {
    _model = model;
    
    self.leftLabel.text = model.leftContent;
    self.rightLabel.text = model.rightContent;
}
@end
  
// ViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TestCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    cell.model = _persons[indexPath.row];
    return cell;
}

// TestTableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TestCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    cell.model = _users[indexPath.row];
    return cell;
}
Copy the code
  • These are the main steps of my protocol-oriented solution to the problem, and we are doneModelViewThe decoupling between them is also hiddenCellThe details have also been improvedCellThe next time you have different data, you just need the model to comply with the protocol that we define and implement the methods in the protocol.

Decoupling from this protocol-oriented approach reminds me of an architecture called MVVM, where the protocol plays the role of VM. The VM decouples the Model from the View, with the View facing the VM, the Model facing the VM, and the Controller facing the VM. We can also slim down the Controller by encapsulating operations that fetch data or more into the VM.

Demo