How to write a UITableView

If you think that there are a lot of methods in UITableViewDelegate and UITableViewDataSource protocols, they are copy and paste every time, which are similar to each other in implementation; if you think that it takes a lot of code to initiate a network request and analyze the data, plus the complexity explosion after refreshing and loading, if you want to know why the following code can meet all the above requirements:

 
VC after decoupling

Fasten your seat belt and get in the car!

MVC

Before discussing decoupling, we need to understand the core of MVC: controller (hereinafter referred to as C) is responsible for the interaction between model (hereinafter referred to as M) and view (hereinafter referred to as V).

M, as mentioned here, is usually not a single class. In many cases, it is a layer composed of multiple classes. The top level is usually a class that ends with a Model, which is held directly by C. The Model class can also hold two objects:

  1. Item: it is the object that actually stores the data. It can be understood as a dictionary, corresponding to the attributes in V
  2. Cache: it can cache its own items (if there are many)

Common mistakes:

  1. In general, data processing will be in M rather than C (C only does things that cannot be reused)
  2. Decoupling doesn't just take a piece of code out there. It's about whether you can merge duplicate code and have good drag and drop performance.

Original version

In C, we create a UITableView object and set its data source and proxy to ourselves. That is, they manage the UI logic and data access logic. Under this framework, there are mainly these problems:

  1. Contrary to the MVC pattern, now V holds C and M.
  2. C manages all the logic, and the coupling is too serious.
  3. In fact, most UI related tasks are done by Cell rather than UITableView itself.

To solve these problems, let's first understand what data sources and agents do respectively.

data source

It has two proxy methods that must be implemented:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

In short, as long as these two methods are implemented, a simple UITableView object is completed.

In addition, it is also responsible for managing the number of section s, titles, editing and moving of a cell, etc.

agent

The agency mainly involves the following aspects:

  1. cell, headerView and so on display the pre and post callbacks.
  2. Height of cell, headerView, etc., click event.

Two methods are most commonly used:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

Note: most proxy methods have an indexPath parameter

Optimize data sources

The simplest idea is to take the data source out as an object.

This method can decouple and reduce the amount of code in C. However, the total amount of code will increase. Our goal is to reduce unnecessary code.

For example, to get the number of rows of each section, its implementation logic is always highly similar. However, the specific implementation of data sources is not uniform, so each data source should be re implemented.

SectionObject

First of all, let's think about a question: what kind of Item does data source hold as M? The answer is a two-dimensional array, where each element holds all the information needed for a section. Therefore, in addition to its own array (for cell), there are section titles, etc. we name such elements SectionObject:

@interface KtTableViewSectionObject : NSObject

@property (nonatomic, copy) NSString *headerTitle; // The titleForHeaderInSection method in the UITableDataSource protocol may use the
@property (nonatomic, copy) NSString *footerTitle; // The titleForFooterInSection method in UITableDataSource protocol may use

@property (nonatomic, retain) NSMutableArray *items;

- (instancetype)initWithItemArray:(NSMutableArray *)items;

@end

Item

The items array should store the items required by each Cell. Considering the characteristics of Cell, the BaseItem of the base class can be designed as follows:

@interface KtTableViewBaseItem : NSObject

@property (nonatomic, retain) NSString *itemIdentifier;
@property (nonatomic, retain) UIImage *itemImage;
@property (nonatomic, retain) NSString *itemTitle;
@property (nonatomic, retain) NSString *itemSubtitle;
@property (nonatomic, retain) UIImage *itemAccessoryImage;

- (instancetype)initWithImage:(UIImage *)image Title:(NSString *)title SubTitle:(NSString *)subTitle AccessoryImage:(UIImage *)accessoryImage;

@end

Parent class implementation code

After the unified data storage format is specified, we can consider to complete some methods in the base class. Take - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section method as an example. It can be implemented as follows:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.sections.count > section) {
        KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section];
        return sectionObject.items.count;
    }
    return 0;
}

It is more difficult to create a cell, because we do not know the type of cell, so naturally we cannot call the alloc method. In addition, in addition to cell creation, you need to set up the UI. These are all things that data sources should not do.

The solutions to these two problems are as follows:

  1. Define a protocol. The parent class returns the base class Cell, and the child class returns the appropriate type as appropriate.
  2. Add a setObject method for Cell to parse the Item and update the UI.

advantage

After such a struggle, the benefits are quite obvious:

  1. The data source of the subclass only needs to implement the cellClassForObject method. The original data source method has been implemented uniformly in the parent class.
  2. Each Cell just needs to write its own setObject method and wait for it to be created and called.
  3. Subclass can get item quickly through objectForRowAtIndexPath method without rewriting.

Control demo (SHA-1:6475496), feel the effect.

Optimization agent

Let's take two commonly used methods in agent protocol as examples to see how to optimize and decouple.

The first is the calculation height. This logic is not necessarily completed in C. because it involves the UI, the Cell is responsible for the implementation. The calculation height is based on Object, so we add a class method to the Cell of the base class:

+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;

Another kind of problem is the agent method represented by handling click event. Their main feature is that they all have indexPath parameter to represent the location. However, in the actual processing, we do not care about the location, but the data in this location.

Therefore, we make a layer of encapsulation to the proxy methods, so that the methods called by C are all with data parameters. Because this data object can be obtained from the data source, we need to be able to obtain the data source object in the proxy method.

To achieve this, the best way is to inherit UITableView:

@protocol KtTableViewDelegate<UITableViewDelegate>

@optional

- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;
- (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section;

// In the future, you can have cell editing, exchange, left slip and other callbacks
// This protocol inherits UITableViewDelegate, so VC still needs to implement a

@end

@interface KtBaseTableView : UITableView<UITableViewDelegate>

@property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource;
@property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate;

@end

The implementation of cell height is as follows: call the method of data source to obtain the data:

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {
    id<KtTableViewDataSource> dataSource = (id<KtTableViewDataSource>)tableView.dataSource;

    KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
    Class cls = [dataSource tableView:tableView cellClassForObject:object];

    return [cls tableView:tableView rowHeightForObject:object];
}

advantage

By encapsulating UITableViewDelegate (mainly through UITableView), we get the following features:

  1. C don't care about Cell height. Each Cell class is responsible for this
  2. If the data itself exists in the data source, then it can be passed to C in the proxy protocol, which avoids the operation of C accessing the data source again.
  3. If the data does not exist in the data source, the method of the proxy protocol will be forwarded normally (because the custom proxy protocol inherits from UITableViewDelegate)

Compare with demo (SHA-1: ca9b261), and feel the effect.

More MVC, more concise

In the above two encapsulation, in fact, we changed the original proxy and data source held by UITableView to the custom proxy and data source held by KtTableView. And many system methods are implemented by default.

So far, it seems that everything has been completed, but there are still some things that can be improved:

  1. It's still not MVC mode!
  2. The logic and implementation of C can still be further simplified

Based on the above considerations, we implement a subclass of UIViewController and encapsulate the data source and agent into C.

@interface KtTableViewController : UIViewController<KtTableViewDelegate, KtTableViewControllerDelegate>

@property (nonatomic, strong) KtBaseTableView *tableView;
@property (nonatomic, strong) KtTableViewDataSource *dataSource;
@property (nonatomic, assign) UITableViewStyle tableViewStyle; // To create a tableView

- (instancetype)initWithStyle:(UITableViewStyle)style;

@end

To ensure that the subclass creates the data source, we define this method into the protocol and define it as required.

Results and objectives

Now let's sort out how to use the modified TableView:

  1. First you need to create a view controller that inherits from KtTableViewController and call its initWithStyle method.

    objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];

  2. In the subclass VC, the createDataSource method is implemented to bind the data source.

    *   (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // This step creates the data source}```
    
  3. In the data source, you need to specify the type of cell.

    *   (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; } ```
    
  4. In Cell, you need to update the UI and return to your height by parsing the data.

    *   (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // The setObject method of the parent class is used in Demo. ` ` ` `
    
    

What else to optimize

So far, we have implemented the encapsulation of UITableView and related protocols and methods, making it easier to use and avoiding a lot of repetitive and meaningless code.

When using, we need to create a controller, a data source, and a custom Cell, which are exactly based on MVC mode. Therefore, it can be said that in terms of encapsulation and decoupling, we have done quite well, even if we make great efforts, it is difficult to improve significantly.

But the discussion about UITableView is far from over. I have listed the following problems to be solved

  1. In this design, the data return is not convenient, for example, the cell sends a message to C.
  2. How to integrate pull-down refresh and pull-up loading
  3. How to integrate network request initiation with analytical data

As for the first problem, it is actually the interaction between V and C in the ordinary MVC mode. You can add the week attribute in Cell (or other classes) to achieve the purpose of direct holding, or you can define the protocol.

Question 2 and question 3 are another big topic. Everyone can implement the network request. But how to integrate into the framework gracefully and ensure the simplicity and expandability of the code is a problem worthy of deep thinking and research. Next, we will focus on network requests.

Why create network layer

How to design an iOS network layer framework? This is a very broad and beyond my ability. There are some excellent and mature ideas and solutions in the industry. Due to the limitation of capabilities and roles, I decided to talk about how to design a common and simple network layer from the perspective of an ordinary developer rather than an architect. I believe that no matter how complex the architecture is, it is also evolved from simple design.

For the vast majority of small-scale applications, the integration of network request framework such as afnetworking is enough to meet more than 99% of the demand. However, with the expansion of the project, or in the long run, the following problems should be directly addressed in the VC: calling the specific network framework (AFNetworking as an example).

  1. Once AFNetworking stops maintenance in the future, and we need to replace the network framework, the cost will be unimaginable. All VC to change the code, and the vast majority of changes are the same.

    For example, ASIHTTPRequest is still used in our project. It can be predicted that this framework will be replaced sooner or later.

  2. The existing framework may not meet our needs. Take ASIHTTPRequest for example. Its underlying layer uses NSOperation to represent every network request. As we all know, the cancellation of an NSOperation is not just a simple call to the cancel method. Without modifying the source code, once it is put into the queue, it cannot be cancelled.

  3. Sometimes our demand is just for network request, and we will expand the request by various customization. For example, we may need to count the start and end time of the request, so as to calculate the time-consuming of the steps of network request and data analysis. Sometimes, we want to design a common component and support each business department to customize specific rules. For example, different departments may add different headers for HTTP requests.

  4. Network requests may have other requirements that need to be added widely, such as pop-up window in case of request failure, logging in case of request, etc.

Refer to the current code (SHA-1:a55ef42) to feel the design without any network layer.

How to design network layer

The solution is very simple:

All computer problems can be solved by adding a middle tier

Readers can think for themselves why adding middle tier can solve the above three problems.

Three modules

For a network framework, I think there are three aspects worth designing:

  1. How to request
  2. How to callback
  3. Data parsing

A complete network request is generally composed of the above three modules. We analyze each module's implementation considerations one by one:

Initiate request

When initiating a request, there are generally two ways of thinking. The first is to write all the parameters to be configured into the same method and borrow them Advancing with the times, the design of iOS network layer architecture under HTTP/2 The code in this paper indicates:

+ (void)networkTransferWithURLString:(NSString *)urlString
                       andParameters:(NSDictionary *)parameters
                              isPOST:(BOOL)isPost
                        transferType:(NETWORK_TRANSFER_TYPE)transferType
                   andSuccessHandler:(void (^)(id responseObject))successHandler
                   andFailureHandler:(void (^)(NSError *error))failureHandler {
                           // Package AFN
                   }

The advantage of this writing method is that all parameters are clear at a glance, and it is easy to use. You can call this method every time. However, the disadvantages are also obvious. With the increase of parameters and calls, the code of network request quickly explodes.

Another set of methods is to set the API as an object and take the parameters to be passed in as the properties of the object. When initiating a request, set up the relevant attributes of the object, and then invoke a simple method.

@interface DRDBaseAPI : NSObject
@property (nonatomic, copy, nullable) NSString *baseUrl;
@property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject,  NSError * _Nullable error);

- (void)start;
- (void)cancel;
...

@end

According to the concept of Model and Item mentioned above, it should be thought that the API object used to access the network is actually a property of Model.

The Model is responsible for exposing the necessary properties and methods, while the specific network requests are completed by the API objects, and the Model should also hold the items that are actually used to store data.

How to callback

The return result of a network request should be a string in JSON format, which can be converted into a dictionary through the system or some open source frameworks.

Next, we need to use runtime related methods to convert dictionaries into Item objects.

Finally, the Model needs to assign this Item to its own property to complete the entire network request.

If from a global point of view, we also need a callback for Model request completion, so that VC can have the opportunity to do the corresponding processing.

Considering the advantages and disadvantages of Block and Delegate, we choose Block to complete the callback.

Data parsing

This part mainly uses runtime to convert the dictionary into an Item. Its implementation is not difficult, but how to hide the implementation details so that the upper business does not care too much is a problem we need to consider.

We can define the Item of a base class and define a parseData function for it:

// KtBaseItem.m
- (void)parseData:(NSDictionary *)data {
    // Parse the data dictionary and assign values to your own attributes
    // See the following article for specific implementation
}

Encapsulate API objects

First, we encapsulate a KtBaseServerAPI object, which has three main purposes:

  1. Isolate the implementation details of the specific network library and provide a stable interface for the upper layer
  2. You can customize some properties, such as the status of the network request, the returned data, and so on, which can be called conveniently
  3. Handle some common logic, such as network time consumption statistics

For specific implementation, please refer to Git submission history: SHA-1:76487f7

Model and Item

BaseModel

Model is mainly responsible for initiating network requests and processing callbacks. Let's see how the model of the base class is defined:

@interface KtBaseModel

// Request callback
@property (nonatomic, copy) KtModelBlock completionBlock;
//Network request
@property (nonatomic,retain) KtBaseServerAPI *serverApi;
//Network request parameters
@property (nonatomic,retain) NSDictionary *params;
//Request address needs to be initialized in subclass init
@property (nonatomic,copy)   NSString *address;
//model cache
@property (retain,nonatomic) KtCache *ktCache;

It can customize its own storage logic and control the selection of request modes (long and short links, JSON or protobuf) by holding API objects to complete network requests.

Model should expose a very simple calling interface to the upper layer, because suppose a model corresponds to a URL, in fact, each request only needs to set parameters, and then it can call the appropriate method to initiate the request.

Because we can't predict when the request will end, we need to set the callback when the request is finished, which also needs to be a property of the Model.

BaseItem

The Item of the base class is mainly responsible for the mapping from property name to json path and the parsing of json data. The core implementation of dictionary transformation model is as follows:

- (void)parseData:(NSDictionary *)data {
    Class cls = [self class];
    while (cls != [KtBaseItem class]) {
        NSDictionary *propertyList = [[KtClassHelper sharedInstance] propertyList:cls];
        for (NSString *key in [propertyList allKeys]) {
            NSString *typeString = [propertyList objectForKey:key];
            NSString* path = [self.jsonDataMap objectForKey:key];
            id value = [data objectAtPath:path];

            [self setfieldName:key fieldClassName:typeString value:value];
        }
        cls = class_getSuperclass(cls);
    }
}

Full code reference Git submission history: SHA-1: 77c6392

How to use

In actual use, first of all, we need to create the module and Item of the subclass. The Model of the subclass should hold the Item object and assign the JSON data carried in the API to the Item object when the network requests a callback.

This JSON object transformation process is implemented in the Item of the base class. When creating the Item of a subclass, you need to specify the corresponding relationship between the property name and the JSON path.

For the upper layer, it needs to generate a Model object, set its path and callback. This callback is usually the VC operation when the network request returns, such as calling the reloadata method. At this time, the VC can confirm that the data requested by the network exists in the Item object held by the Model.

Refer to Git submission history for specific code: SHA-1:8981e28

Drop-down refresh

Many applications of UITableview have the functions of pull-down refresh and pull-up loading. When implementing this function, we mainly consider two points:

  1. Hide the underlying implementation details and expose stable and easy-to-use interfaces
  2. How to implement Model and Item

The first point is a platitude. You can see how to implement a simple package by referring to SHA-1 61ba974.

The focus is on the transformation of Model and Item.

ListItem

This Item has no other function. It defines a property pageNumber, which needs to be negotiated with the server. The Model will judge whether it has all been loaded according to this property.

// In .h
@interface KtBaseListItem : KtBaseItem

@property (nonatomic, assign) int pageNumber;

@end

// In .m
- (id)initWithData:(NSDictionary *)data {
    if (self = [super initWithData:data]) {
        self.pageNumber = [[NSString stringWithFormat:@"%@", [data objectForKey:@"page_number"]] intValue];
    }
    return self;
}

For the Server, it is very inefficient to return page u number every time, because every parameter may be different, so calculating the total data amount is a very time-consuming work. Therefore, in actual use, the client can contract with the Server and return the result with the "isHasNext" field. Through this field, we can also determine whether to load to the last page.

ListModel

It holds a ListItem object, exposes a group of loading methods, and defines a protocol, KtBaseListModelProtocol, in which the method is the method to be executed after the end of the request.

@protocol KtBaseListModelProtocol <NSObject>

@required
- (void)refreshRequestDidSuccess;
- (void)loadRequestDidSuccess;
- (void)didLoadLastPage;
- (void)handleAfterRequestFinish; // After the request, refresh the tableview or close the animation.

@optional
- (void)didLoadFirstPage;

@end

@interface KtBaseListModel : KtBaseModel

@property (nonatomic, strong) KtBaseListItem *listItem;
@property (nonatomic, weak) id<KtBaseListModelProtocol> delegate;
@property (nonatomic, assign) BOOL isRefresh; // If yes, refresh, otherwise load.

- (void)loadPage:(int)pageNumber;
- (void)loadNextPage;
- (void)loadPreviousPage;

@end

In fact, when data is added or deleted on the Server side, the parameter nextPage cannot meet the requirements. The pages acquired twice are not completely without intersection. It is likely that they have duplicate elements, so the Model should also shoulder the task of de duplication. In order to simplify the problem, this is not a complete implementation.

RefreshTableViewController

It implements the protocol defined in ListMode and provides some general methods, while the specific business logic is implemented by subclasses.

#pragma -mark KtBaseListModelProtocol
- (void)loadRequestDidSuccess {
    [self requestDidSuccess];
}

- (void)refreshRequestDidSuccess {
    [self.dataSource clearAllItems];
    [self requestDidSuccess];
}

- (void)handleAfterRequestFinish {
    [self.tableView stopRefreshingAnimation];
    [self.tableView reloadData];
}

- (void)didLoadLastPage {
    [self.tableView.mj_footer endRefreshingWithNoMoreData];
}

#pragma -mark KtTableViewDelegate
- (void)pullUpToRefreshAction {
    [self.listModel loadNextPage];
}

- (void)pullDownToRefreshAction {
    [self.listModel refresh];
}

Actual use

In a VC, it only needs to inherit RefreshTableViewController and implement the requestDidSuccess method. Here is the complete code of VC, which is extraordinarily simple:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createModel];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)createModel {
    self.listModel = [[KtMainTableModel alloc] initWithAddress:@"/mooclist.php"];
    self.listModel.delegate = self;
}

- (void)createDataSource {
    self.dataSource = [[KtMainTableViewDataSource alloc] init]; // This step creates the data source
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)requestDidSuccess {
    for (KtMainTableBookItem *book in ((KtMainTableModel *)self.listModel).tableViewItem.books) {
        KtTableViewBaseItem *item = [[KtTableViewBaseItem alloc] init];
        item.itemTitle = book.bookTitle;
        [self.dataSource appendItem:item];
    }
}

Other judgments, such as closing the animation at the end of the request, prompt on the last page that there is no more data, and public logic such as pull-down refresh and pull-up load trigger methods have been implemented by the parent class.

See Git submission history: SHA-1:0555db2 for specific code

Write at the end

This is the end of the design architecture of network request, which has many value expansion places. As the old saying goes, there is no general architecture, only the most suitable one for business.

In order to facilitate demonstration and reading, my Demo usually implements the underlying classes and methods first, and then is called by the upper layer. But in fact, this approach is not realistic in the actual development. We always find a lot of redundant, meaningless code before we start to design the architecture.

So in my opinion, the real architectural process is that when the business changes (usually becomes more complex), we should start to think about which operations can be omitted (implemented by the parent class or agent) and how the top layer should call the underlying services. Once the top-level call mode is designed, it can be implemented to the bottom level step by step.

Due to the limited level of the author, the framework of this article is not excellent. I hope to share the harvest with you after I have a deep understanding of the design mode and accumulated more experience.

As a developer, it is particularly important to have a learning atmosphere and a communication circle. This is my iOS communication group: 1012951431 Whether you are Xiaobai or Daniel, welcome to join us, share BAT, Ali interview questions, interview experience, discuss technology, and let's exchange, learn and grow together!

Another copy of the interview questions collected by all friends is attached. You can download them by yourself!

 

Tags: iOS network JSON git

Posted on Wed, 18 Mar 2020 03:44:47 -0700 by storyboo