How to write a UITableView

This article is a simple text arrangement for live sharing. The live broadcast is divided into two parts. The first part: Youku Or YouTube, the second part: Youku Demo address: KtTableView

If you think that the UITableViewDelegate and UITableViewDataSource of the two protocols in a large number of methods each copy and paste, to achieve the same; if you think cyber requests and parse data needs a large block of code, plus the refresh and after loading is the complexity of the burst table, if you want to know why the following code can meet all the above requirements:

How to write a UITableView
decoupled VC

Fasten your seat belt and get on the bus!

MVC

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

The M is often not a single class, and in many cases it is a layer made up of several classes. The uppermost class, usually ending in Model, which is held directly by C. The Model class can also hold two objects:

  1. Item: it is the object of the actual storage of data. It can be interpreted as a dictionary, corresponding to the attributes in V
  2. Cache: it can cache its own Item (if there are many)

Common mistakes:

  1. In general, data processing is placed on M rather than C (C only does things that can not be reused)
  2. Decoupling doesn’t just take a piece of code out. Instead, focus on whether you can merge duplicate code and have good scalability.

Original edition

In C, we create the UITableView object and then set its data source and proxy to itself. That is, the logic of managing UI logic and data access by itself. In this framework, these problems are the main ones:

  1. Contrary to the MVC pattern, V is now holding C and M.
  2. C manages all the logic, coupling too seriously.
  3. In fact, the vast majority of UI correlations are done by Cell rather than by UITableView itself.

To solve these problems, we first figure out what the data source and the agent did respectively.

data source

It has two proxy methods that have to be implemented:

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

Simply put, as long as the two methods are implemented, a simple UITableView object is complete.

In addition, it manages the amount of section, the title, the editing and moving of a cell, and so on.

agent

The agency mainly involves the following aspects:

  1. Cell, headerView and so on display before and after the callback.
  2. Cell, headerView, etc height, click event.

The most commonly used are also the two methods:

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

Reminder: most proxy methods have a indexPath parameter

Optimize data source

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

This method has some decoupling effect, and can effectively reduce the amount of code in C. However, the total amount of code will rise. Our goal is to reduce unnecessary code.

For example, to obtain the number of rows per section, its implementation logic is always highly similar. However, since the specific implementation of the data source is not uniform, each data source needs to be re – implemented.

SectionObject

First, let’s consider a question, what does a data source look like as a M and what Item it holds? The answer is a two-dimensional array, and each element holds all the information needed by a section. So, in addition to having our own array (for cell), and the title of section, we call this element SectionObject:

@interface KtTableViewSectionObject: NSObject @property (nonatomic, copy) NSString *headerTitle; / / titleForHeaderInSection method in the UITableDataSource protocol may be used in @property (nonatomic, copy) NSString *footerTitle; / / titleForFooterInSection method in the UITableDataSource protocol may be used in @property (nonatomic, retain) NSMutableArray *items; (instancetype) - initWithItemArray: (NSMutableArray * items); @end

Item

The items array in it should store the Item needed for each cell, and the BaseItem of the base class can be designed to take into account the characteristics of the Cell:

@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

Given the uniform data storage format, we can consider doing some of the methods in the base class. Take – (NSInteger) tableView: (UITableView *) tableView numberOfRowsInSection: (NSInteger) section method as an example, it can be achieved like this:

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

The more difficult thing is to create cell because we do not know the type of cell and naturally cannot invoke the alloc method. In addition to this, cell needs to set up UI in addition to creation, all of which data sources should not do.

The solution to these two problems is as follows:

  1. Defines a protocol that returns the base class Cell, and the subclass returns the appropriate type as the case is.
  2. Add a setObject method for Cell to parse the Item and update the UI.

advantage

After all this agonizing, the benefits are quite obvious:

  1. Subclasses of data sources only need to implement the cellClassForObject method. The original data source method has been implemented in the parent class.
  2. Every Cell just writes its own setObject method, and then sits back and sits and is called.
  3. Subclasses can quickly obtain item through the objectForRowAtIndexPath method without rewriting.

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

Optimization agent

Let’s take the two methods commonly used in the agency agreement as examples to see how to optimize and decouple.

The first is to calculate the height. This logic is not necessarily done in C. Because it involves UI, the Cell is responsible for the implementation. The height is calculated on the basis of Object, so we add a class method to the Cell of the base class:

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

Another class of problems is proxy methods that deal with click events. Their main feature is that indexPath parameters are used to represent locations. However, in actual processing, we do not concern the location and care about the data at this location.

Therefore, we encapsulate the proxy method in a way that makes C calls 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.

The best way to do this is to inherit UITableView:

@protocol KtTableViewDelegate< UITableViewDelegate> @optional - (void) didSelectObject: (ID) object atIndexPath: (NSIndexPath*) indexPath; - (UIView *) headerViewForSectionObject: (KtTableViewSectionObject * sectionObject) atSection: section (NSInteger); / / exchange editor, I can have cell, left slip / / this protocol inherits the UITableViewDelegate callback, so do a layer transfer yourself, VC still need to implement a @end @interface KtBaseTableView: UITableView< UITableViewDelegate&; gt; @property (nonatomic, assign) id< KtTableViewDataSource> ktDataSource; @property (nonatomic, assign) id< KtTableViewDelegate> ktDelegate; @end

The implementation of the cell height is as follows: the method of calling the data source gets 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 the UITableViewDelegate (in fact, mostly through UITableView), we have the following characteristics:

  1. C doesn’t have to worry about Cell height, which is responsible for each Cell class itself
  2. If the data itself is present in the data source, then in the proxy protocol, it can be passed to the C, eliminating the need for C to re access the data source.
  3. If the data does not exist in the data source, then the method of the proxy protocol is forwarded normally (because the custom proxy protocol is inherited from UITableViewDelegate)

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

More MVC, more concise

In the last two packages, we actually switched UITableView to native agents and data sources and changed it to KtTableView, with custom proxies and data sources. And many methods of the system are implemented by default.

So far, everything seems to have been done, but in fact there are still some areas 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 data sources and agents into C.

@interface KtTableViewController: UIViewController< KtTableViewDelegate, KtTableViewControllerDelegate> @property (nonatomic, strong) KtBaseTableView *tableView @property (nonatomic, strong); KtTableViewDataSource *dataSource; @property UITableViewStyle tableViewStyle (nonatomic, assign); / / used to create tableView - (instancetype) initWithStyle: (UITableViewStyle) style; @end

To ensure that subclasses create data sources, we define this method into the protocol and define it as required.

Achievements and goals

Now let’s figure out what to do with the transformed TableView:

  1. First of all, you need to create a view controller that inherits from KtTableViewController and call its initWithStyle method. KTMainViewController *mainVC = [[KTMainViewController, alloc], initWithStyle:UITableViewStylePlain];
  2. Implement the createDataSource method in subclass VC to implement the binding of data sources. – (void) createDataSource [[KtMainTableViewDataSource alloc] {self.dataSource = init]; / / this step to create a 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 by parsing the data and return to your height. (CGFloat) + tableView: (UITableView * tableView) rowHeightForObject: (KtTableViewBaseItem * object) {return 60;} / / Demo follows the superclass method setObject.
  5. In this design, the data is not easy to return, such as cell’s message to C.

Anything else to optimize?

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

In use, we need to create a controller, a data source, and a custom Cell, which is exactly based on the MVC schema. Therefore, we can say that in packaging and decoupling, we have done quite well, even if it is still a great effort, it is difficult to improve significantly.

But the discussion about UITableView is far from over, and I listed the following issues that need to be addressed

  1. How to integrate drop down refresh and pull up loading
  2. The initiation of network requests and how to integrate them with parsed data
  3. Once AFNetworking to stop the maintenance day, and we need to replace the network framework, the cost will not be able to imagine. All VC changes the code, and most changes are the same. Such examples really exist, for example, in our project, the ASIHTTPRequest, which has long been discontinued, is still used, and it can be expected that the framework will be replaced sooner or later.

The first question, in fact, is the interaction of V and C in the normal MVC schema, where weak attributes can be added to Cell (or other classes) to achieve direct holding or protocol definitions.

Questions two and three are another big topic. Network requests will be implemented by everyone, but how to elegantly integrate into the framework and ensure the simplicity and expansion of the code is a question worth thinking deeply and studying. Next, we’ll focus on network requests.

Why create network layer?

How should a iOS’s network layer framework be designed? This is a very broad and beyond my ability. The industry has some excellent, mature ideas and solutions, due to limited capacity, the role, I decided from an ordinary developer rather than the architect’s perspective, say, a network layer of ordinary and simple to design. I believe that the complex architecture evolved from simple design.

For most small applications, the network request framework such as the integration of AFNetworking is enough to meet more than 99% of the requirements. But as the project expands, or in the long run, the specific network framework is invoked directly in VC (below, for example, AFNetworking), with at least the following problems:

  1. Existing frameworks may not be able to meet our needs. Take ASIHTTPRequest as an example, its bottom layer uses NSOperation to represent each network request. As you all know, the cancellation of a NSOperation is not simply a call to the cancel method. Without modifying the source code, once it is placed in the queue, it is virtually impossible to cancel.
  2. Sometimes our requirements are just network requests, and various customizations can be extended to this request. For example, we may calculate the initiation and end times of requests, thereby calculating the network requests and the time consuming of the steps of data parsing. Sometimes, we want to design a generic component and support custom rules defined by each business unit. For example, different departments may add different heads for the HTTP request.
  3. Network requests can also have other requirements that need to be added, such as popups when requests fail, logging when requested, and so on.
  4. How to request

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

How to design network layer

In fact, the solution is very simple:

All computer problems can be solved by adding an intermediate layer

Readers can think for themselves, why adding the intermediate layer can solve the three problems mentioned above.

Three modules

As far as a network framework is concerned, I think there are three main aspects that are worth designing:

  1. How to callback
  2. Data analysis
  3. Isolation of specific implementation details of the network library, providing a stable interface for the upper layer

A complete network request generally consists of the above three modules, and we analyze each module when the implementation of the notes:

Initiate request

When you start a request, there are generally two ways of thinking. The first is to write all the parameters that you want to configure into the same method, and borrow the code representation in the text of the iOS network layer architecture, which is advancing with the times and under the HTTP/2:

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

The advantage of this approach is that all parameters are clear and easy to use, and this method is called every time. But the drawback is also obvious, as the number of arguments and calls increases, the code for network requests is quickly exploded.

Another set of methods is to set API to an object that uses the parameters to be passed as attributes of the object. When you start a request, simply set the object’s associated properties and call 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) - (void) start; cancel; @end...

According to the concepts of Model and Item mentioned earlier, it should be possible to think that the API object used to access the network is actually an attribute of Model.

Model is responsible for external exposure of the necessary attributes and methods, while the specific network request is done by the API object, and the Model should also hold Item which is really used to store data.

How to callback

The return result of a network request should be a JSON formatted string that can be converted into a dictionary through a system or some open source framework.

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

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

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

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

Data analysis

This part mainly uses runtime to translate the dictionary into Item, and its implementation is not difficult, but how to hide the details of implementation and make the top business do not care too much is the question we should consider.

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

/ / KtBaseItem.m - (void) parseData: (NSDictionary * data) {/ / data for parsing this dictionary, see behind the realization of their own specific attribute assignment / article}

Encapsulating API objects

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

  1. You can customize some properties, such as the status of the network request, the returned data, and so on
  2. Deal with some common logic, such as network consumption statistics
  3. Hide the underlying implementation details, exposing external, stable, easy-to-use interfaces

For specific implementations, refer to the Git submission history: SHA-1:76487f7

Model and Item

BaseModel

Model primarily needs to be responsible for initiating network requests and handling callbacks, to see how the base class’s Model is defined:

@interface KtBaseModel / / @property request callback (nonatomic, copy) KtModelBlock completionBlock; / / network (nonatomic, retain) @property request KtBaseServerAPI *serverApi; / / network request parameter @property (nonatomic, retain) NSDictionary * params; / / request address needs in the subclass init to initialize the @property (nonatomic, copy) NSString //model @property (retain *address; cache KtCache *ktCache, nonatomic);

It allows you to customize your storage logic by holding API objects, complete network requests, and control the choice of requests (long, short links, JSON or protobuf).

Model should be exposed to a very simple interface to upper layer, because of the assumption that a Model corresponding to a URL, in fact, each request only need to set the parameters, you can request a call the appropriate method.

Since we cannot predict when the request will end, we need to set the callback for the completion of the request, which also needs to be a property of the Model.

BaseItem

The base class’s Item is primarily responsible for the mapping of property name to JSON path, and the parsing of JSON data. The core dictionary conversion model is implemented 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 [propertyList objectForKey:key]) {*typeString = NSString*; path = [self.jsonDataMap objectForKey:key]; ID value = [data objectAtPath:path]; [self setfieldName:key fieldClassName:typeString value:value] CLS = class_getSuperclass;}}} (CLS);

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

How to use

In actual use, you first create Modle and Item for subclasses. The subclass Model should hold the Item object and assign the JSON data in the API to the Item object when the network requests the callback.

The JSON transformation process is implemented in the base class Item, and when the subclass Item is created, you need to specify the corresponding relationship between the property name and the JSON path.

For the upper level, it needs to generate a Model object, set its path, and the callback, which is typically the operation of the VC when the network request returns, such as invoking the reloadData method. At this point the VC can determine that the data requested by the network exists in the Item object held by Model.

Specific code reference Git submission history: SHA-1:8981e28

Pull-down refresh

Many applications of UITableview have the functions of pull down, refresh and pull loading, and we mainly consider two points when we implement this function:

  1. How can Model and Item be implemented?
  2. Model 和 Item 如何实现

The first point is a truism, and you can see how to implement a simple package with reference to SHA-1 61ba974.

The focus is on the transformation of the Model and the Item.

ListItem

This Item has nothing else to do with defining an attribute, pageNumber, which needs to be negotiated with the server. Model will determine whether all the loads have been loaded based on this property.

.h @interface: KtBaseItem / / In KtBaseListItem @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 Server, it would be very inefficient to return page_number every time, because each parameter might be different, and computing the total amount of data would be a very time-consuming task. Therefore, in actual use, the client can agree with the Server and return the result with the isHasNext field. Through this field, we can also decide whether to load the last page.

ListModel

It holds a ListItem object, exposes a set of loading methods externally, and defines a protocol called KtBaseListModelProtocol, which is the method that will be executed after the request ends.

@protocol KtBaseListModelProtocol < NSObject> refreshRequestDidSuccess; @required - (void) - (void) loadRequestDidSuccess; (void) - didLoadLastPage - (void); handleAfterRequestFinish; / / request after the end of the operation, refresh tableview or off 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, said refresh, otherwise it is loaded. - - (void) loadPage: (int) pageNumber; - - (void) loadNextPage; - - (void) loadPreviousPage; @end

In fact, when the data is added and deleted at the Server end, it is not possible to pass only the parameter nextPage. The two page is not completely without intersection, it is likely that they have repeated elements, so Model should also shoulder the heavy task. To simplify the problem, this is not complete.

RefreshTableViewController

It implements the protocol defined in ListMode and provides some general methods and specific business logic is implemented by a descendant class.

#pragma -mark KtBaseListModelProtocol (void) loadRequestDidSuccess {[self requestDidSuccess];} - {[self.dataSource (void) refreshRequestDidSuccess clearAllItems]; [self requestDidSuccess];} - {[(void) handleAfterRequestFinish self.tableView stopRefreshingAnimation]; [self.tableView reloadData];} - (void) didLoadLastPage endRefreshingWithNoMoreData] #pragma -mark {[self.tableView.mj_footer}; KtTableViewDelegate (void pullUpToRefreshAction) {[self.listModel} - loadNextPage]; (void) pullDownToRefreshAction {[self.listModel refresh];}

Practical use

In a VC, it simply inherits RefreshTableViewController and then implements the requestDidSuccess method. Here’s a complete code for VC, which is extraordinarily simple:

- (void) viewDidLoad viewDidLoad] [self {[super; 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;} - {self.dataSource = createDataSource (void) [[KtMainTableViewDataSource alloc] init]; / / this step create a 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) *item alloc] init] {KtTableViewBaseItem = [[KtTableViewBaseItem; Item.itemTitle = book.bookTitle; [self.dataSource appendItem:item];}

Other judgments, such as closing the animation at the end of the request, the last page prompt, no more data, the public logic such as pull-down, refresh, and pull loading triggers, have been implemented by the parent class.

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

Written at the end

This is the end of the design framework for web requests, and it has a lot of value to expand. As the old saying goes, there is no universal architecture, but the only architecture that is best suited for business.

In order to facilitate presentation and reading, my Demo usually implements the underlying classes and methods first, and then calls them from the upper level. But in practice, this approach is impractical in actual development. We always begin to design architectures after discovering large amounts of redundant, meaningless code.

So in my opinion, is the real business process architecture when changes occur (usually become complex), we should start thinking what the current operation can be omitted (as implemented by the superclass or agent), the top should call the way in which the underlying service. Once you have designed the top level invocation, you can implement it to the bottom level step by step.

Because the author’s level is limited, the structure of this paper is not good. I hope to share the experience after I have a better understanding of the design pattern and accumulate more experience.