Update record:

2017.4.23: Added support for data sources to rely entirely on network requests. ** 2017.4.22: Added support for refreshing tables after requesting new data. ** ** 2017.4.21: Added CocoaPods support: pod ‘SJStaticTableView’, ‘~> 1.2.0’. **


Write a little wheel

When writing a UITableView, we often encounter a requirement that requires a custom dynamic cell (such as a list of Twitter posts) that is entirely dependent on network requests. But at the same time, most apps also have Settings pages, personal pages, and other static table-based pages.

And these pages have more in common:

  1. In most cases, you get all the data before you even get to the page.
  2. The cell style is simple, and the custom cell is rare (almost all cells are 44 in height).
  3. Most of them are grouped.

Because I really want to write an open source thing (can also expose their own shortcomings), and at the same time limited by the level, so I decided to write a relatively simple, but also universal framework: a high degree of customization suitable for the use of personal pages and Settings page UITableView.

Before writing, I read several similar articles and picked out three that I thought were better:

  1. Clean Table View Code
  2. How do I write a UITableView
  3. Use MVVM design to quickly develop modules such as personal center and Settings

After reading the summary, I wrote this framework in my spare time of 3 days last week. In order to make it practical, I wrote a Demo by imitating the discovery page, personal page and setting page of wechat client, and have a look at the effect picture:

Project resource from: making: zhengwenming/WeChat Demo address: making: knightsj/SJStaticTableView

In order to show the customization of the framework, I added two pages of my own. The entry is in the Settings page:

Let’s not get into the specifics of group and group customization, but I’ll go into more detail when I talk about customization later. I’m just going to show you what it looks like.

Now that you have an overview of the functionality, let’s go through the framework in detail. The reason for writing this introduction is not to hope how many people will use it, but to express my own ideas. If you feel bad, please criticize more.

Before the formal explanation, let’s introduce the basic contents of this article:

  1. Technical points used.
  2. Function description.
  3. Usage method.
  4. Introduction to customization.
  5. The refresh function is added.
  6. New support for data sources to rely entirely on network requests.

1. Technical points used


The framework as a whole is relatively simple, mainly based on Apple’s UITableView component, for decoupling and separation of responsibilities, mainly using the following technical points:

  • MVVM: Using the MVVM architecture, each row of “pure” data is handed to a separateViewModelLet it hold data for each cell (row height, cell type, text width, image height, etc.). And there’s one for each sectionViewModel, which holds configuration data for the current section (title, header, footer height, etc.).
  • Light UIViewControllerSeparation:UITableViewDataSourcewithUIViewController, let a separate class implementUITableViewDataSourceThe function.
  • Block: Use a block to call the cell’s drawing method.
  • Classification: Use the classification to define the drawing method of each different cell.

After knowing the main technical points used, I will introduce the functions of the framework in detail.

2. Function Description


This framework can be used to quickly set up the Settings page, the personal information page can be static table page, the user just need to pass in the tableView DataSource element is the viewModel array.

Although the layout of this type of page is relatively simple, there are still several different cases (cell layout type). I have encapsulated the common cell layout so that users can use it directly.

When defining the types of these cells, I roughly divide them into two categories:

  1. The first type is the system-style cell, in most cases, the cell height is 44; On the left side of the cell there will be a picture, a label, or just one (but rarely only a picture); There is usually a right-facing arrow on the right side of the cell, and sometimes there may be label, image, or both on the left side of the arrow.
  2. The second type is the custom cell. The height of the cell may not be 44, and the layout of the cell is very different from that of the system style. Users need to add the cell by themselves.

Based on these two categories, several cases are subdivided, which can be visually seen by the following picture:

The enumeration of the type needs to be defined in the viewModel of the cell:

typedef NS_ENUM(NSInteger, SJStaticCellType) {
    
    // All cell types of system style are packaged and can be used directly
    SJStaticCellTypeSystemLogout,                          // Exit the login cell
    SJStaticCellTypeSystemAccessoryNone,                   // There are no controls on the right
    SJStaticCellTypeSystemAccessorySwitch,                 // The right side is the switch
    SJStaticCellTypeSystemAccessoryDisclosureIndicator,    // To the right is the triangle arrow (the arrow can have an image or a label, or both, depending on the parameter passed in)
    
    // User-defined cell type to be added by the user
    SJStaticCellTypeMeAvatar,                              // Personal page "ME" cell
};
Copy the code

Just to give you an idea:

There are three points to mention here:

  1. In addition to the custom cells, other types of cells do not need to be laid out by the developer. They have been packaged by me and only need to be laid out in the cellViewModelThe corresponding type and data (text, picture) can be passed in.
  2. Because there is at least one control on the left (picture and text) and the left and right order is fixed (picture is always on the far left), the framework can layout the cell itself by passing in images and text on the left to display. Therefore, type judgment is mainly applied to the right side of the cell.
  3. It is worth mentioning that all the five types in the “right-most is an arrow” subbranch belong to the same type, just need to pass in the text and picture, and the text and picture display order parameter (this parameter is only valid when both picture and text exist) to determine the layout.

Now that we know what the framework does, let’s take a look at how to use it:

3. Usage


Integration method:

  1. Static: manual SJStaticTableViewComponent folder into the project.
  2. Dynamic: CocoaPods:Pod 'SJStaticTableView', '~ > 1.1.2.

The specific method is explained in words first:

  1. ViewController inheritance for the page to be developedSJStaticTableViewController.
  2. In the new ViewControllercreateDataSourceMethod to pass the viewModel array to the controllerdataSourceProperties.
  3. Call different cell drawing methods based on different cell types.
  4. Implement if you need to accept cell clicksdidSelectViewModelMethods.

It may feel abstract, but let me use the Settings page to illustrate:

Take a look at the layout of the Settings page:

Then let’s look at the code for the set ViewController:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @ "Settings";
}


- (void)createDataSource
{
    self.dataSource = [[SJStaticTableViewDataSource alloc] initWithViewModelsArray:[Factory settingPageData] configureBlock:^(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel *viewModel) {
        
        switch (viewModel.staticCellType)
        {
            case SJStaticCellTypeSystemAccessoryDisclosureIndicator:
            {
                [cell configureAccessoryDisclosureIndicatorCellWithViewModel:viewModel];
            }
                break;
                
            case SJStaticCellTypeSystemAccessorySwitch:
            {
                [cell configureAccessorySwitchCellWithViewModel:viewModel];
            }
                break;
                
            case SJStaticCellTypeSystemLogout:
            {
                [cell configureLogoutTableViewCellWithViewModel:viewModel];
            }
                break;
                
            case SJStaticCellTypeSystemAccessoryNone:
            {
                [cell configureAccessoryNoneCellWithViewModel:viewModel];
            }
                break;
                
            default:
                break; }}]; } - (void)didSelectViewModel:(SJStaticTableviewCellViewModel *)viewModel atIndexPath:(NSIndexPath *)indexPath
{
    
    switch (viewModel.identifier)
    {
            
        case 6:
        {
            NSLog(@" Log out");
            [self showAlertWithMessage:Do you really want to log out?];
        }
            break;
            
        case 8:
        {
            NSLog(@" Clear cache");
        }
            break;
            
        case 9:
        {
            NSLog(@" Jump to custom Cell display page - Group");
            SJCustomCellsViewController *vc = [[SJCustomCellsViewController alloc] init];
            [self.navigationController pushViewController:vc animated:YES];
        }
            break;
            
        case 10:
        {
            NSLog(@" Jump to custom cell display page - Same group");
            SJCustomCellsOneSectionViewController *vc = [[SJCustomCellsOneSectionViewController alloc] init];
            [self.navigationController pushViewController:vc animated:YES];
        }
            break;
            
        default:
            break; }}Copy the code

Here are some questions you might have:

  1. Where’s the UITableViewDataSource method?
  2. How is the viewModel array set up?
  3. How are cell drawing methods distinguished?
  4. Where’s the UITableViewDelegate method?

I will answer them one by one. After reading the answers below, you will almost fully grasp the idea of the framework:

Question 1: Where is the UITableViewDataSource method?

My own class encapsulates a SJStaticTableViewDataSource specifically as a data source, need to give it a viewModel array controller.

Take a look at its implementation file:

//SJStaticTableViewDataSource.m
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return self.viewModelsArray.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.cellViewModelsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Get the section ViewModel
    SJStaticTableviewSectionViewModel *sectionViewModel = self.viewModelsArray[indexPath.section];
    // Get the viewModel of the cell
    SJStaticTableviewCellViewModel *cellViewModel = sectionViewModel.cellViewModelsArray[indexPath.row];
    
    SJStaticTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellViewModel.cellID];
    if(! cell) { cell = [[SJStaticTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellViewModel.cellID];
    }
    self.cellConfigureBlock(cell,cellViewModel);
    
    return cell;
    
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.sectionHeaderTitle;  
}

- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.sectionFooterTitle;
}
Copy the code

Each cell and section of the table has a viewModel that encapsulates its data:

The viewModel of the cell:

typedef NS_ENUM(NSInteger, SJStaticCellType) {
    
    // All cell types of system style are packaged and can be used directly
    SJStaticCellTypeSystemLogout,                          // Exit the login cell (encapsulated)
    SJStaticCellTypeSystemAccessoryNone,                   // There are no controls on the right
    SJStaticCellTypeSystemAccessorySwitch,                 // The right side is the switch
    SJStaticCellTypeSystemAccessoryDisclosureIndicator,    // To the right is the triangle arrow (the arrow can have an image or a label, or both, depending on the parameter passed in)
    
    // User-defined cell type to be added by the user
    SJStaticCellTypeMeAvatar,                              // Personal page "ME" cell
    
};


typedef void(^SwitchValueChagedBlock)(BOOL isOn);           //switch The block that is called when the switch is switched


@interface SJStaticTableviewCellViewModel : NSObject

@property (nonatomic.assign) SJStaticCellType staticCellType;                  / / type


@property (nonatomic.copy)   NSString *cellID;                                  //cell reuser identifier
@property (nonatomic.assign) NSInteger identifier;                              // Differentiate each cell for clicking

// =============== The default cell is left =============== //
@property (nonatomic.strong) UIImage  *leftImage;                               // Image on the left, as needed
@property (nonatomic.assign) CGSize leftImageSize;                              // The size of the image on the left, with default Settings

@property (nonatomic.copy)   NSString *leftTitle;                               // Cell main header, passed as needed
@property (nonatomic.strong) UIColor *leftLabelTextColor;                       // The color of the text in the label to the left of the current group cell
@property (nonatomic.strong) UIFont *leftLabelTextFont;                         // The font of the text in the label left of the current group cell

@property (nonatomic.assign) CGFloat leftImageAndLabelGap;                      // The distance between image and label on the left has a default value


// =============== The default cell on the right =============== //
@property (nonatomic.copy)   NSString *indicatorLeftTitle;                      // Text to the left of the right arrow is passed in as needed
@property (nonatomic.strong) UIColor *indicatorLeftLabelTextColor;              // The color of the text on the right has default Settings and can be customized
@property (nonatomic.strong) UIFont *indicatorLeftLabelTextFont;                // The font of the text on the right has default Settings and can also be customized
@property (nonatomic.strong) UIImage *indicatorLeftImage;                       // Image to the left of the right arrow, as needed
@property (nonatomic.assign) CGSize indicatorLeftImageSize;                     // The size of the image on the left of the right tip is default and can be customized

@property (nonatomic.assign.readonly)  BOOL hasIndicatorImageAndLabel;         // Whether the text and image to the left of the right tip exist at the same time can only be calculated internally

@property (nonatomic.assign) CGFloat indicatorLeftImageAndLabelGap;             // The distance between image and label to the left of the right tip, with a default value
@property (nonatomic.assign) BOOL isImageFirst;                                 // If the text to the left of the right tip and image exist together, is image next to the arrow
@property (nonatomic.copy) SwitchValueChagedBlock switchValueDidChangeBlock;    // Block called when switching switch


// =============== long width data =============== //
@property (nonatomic.assign) CGFloat cellHeight;                                // The cell height is 44 by default and can be set
@property (nonatomic.assign) CGSize  leftTitleLabelSize;                        // The left side of the default Label size, after passing text internal calculation
@property (nonatomic.assign) CGSize  indicatorLeftLabelSize;                    // Size of the label on the right


/ / = = = = = = = = = = = = = = = custom cell data here = = = = = = = = = = = = = = = / /
@property (nonatomic.strong) UIImage *avatarImage;
@property (nonatomic.strong) UIImage *codeImage;
@property (nonatomic.copy)   NSString *userName;
@property (nonatomic.copy)   NSString *userID;
Copy the code

The viewModel for the section is as follows:

@interface SJStaticTableviewSectionViewModel : NSObject

@property (nonatomic.copy)   NSString *sectionHeaderTitle;         // The title of the section
@property (nonatomic.copy)   NSString *sectionFooterTitle;         // The title of the section
@property (nonatomic.strong) NSArray  *cellViewModelsArray;        // Data source for this section

@property (nonatomic.assign) CGFloat  sectionHeaderHeight;         / / the height of the header
@property (nonatomic.assign) CGFloat  sectionFooterHeight;         The height of the / / footer

@property (nonatomic.assign) CGSize leftImageSize;                 // The size of the image to the left of the current group of cells
@property (nonatomic.strong) UIColor *leftLabelTextColor;          // The color of the text in the label to the left of the current group cell
@property (nonatomic.strong) UIFont *leftLabelTextFont;            // The font of the text in the label left of the current group cell
@property (nonatomic.assign) CGFloat leftImageAndLabelGap;         // The distance between image and label on the left of the current group, with a default value

@property (nonatomic.strong) UIColor *indicatorLeftLabelTextColor; // The color of the text in the label to the right of the current group cell
@property (nonatomic.strong) UIFont *indicatorLeftLabelTextFont;   // The font of the text in the label to the right of the current group cell
@property (nonatomic.assign) CGSize indicatorLeftImageSize;        // The size of the image on the right of the current group cell
@property (nonatomic.assign) CGFloat indicatorLeftImageAndLabelGap;// The distance between image and label on the right of the current group cell has a default value


- (instancetype)initWithCellViewModelsArray:(NSArray *)cellViewModelsArray;
Copy the code

You might think there are too many attributes, but the purpose of these attributes is to serve cell customization, as explained below.

Now that I’ve wrapped the viewModel of the data source, cell, and section, let’s look at the second question:

Question 2: How is the viewModel array set?

Let’s look at the Settings for the viewModel array of the Settings page:

+ (NSArray *)settingPageData
{
    // ========== section 0
    SJStaticTableviewCellViewModel *vm0 = [[SJStaticTableviewCellViewModel alloc] init];
    vm0.leftTitle = @" Account and Security";
    vm0.identifier = 0;
    vm0.indicatorLeftTitle = @" Protected";
    vm0.indicatorLeftImage = [UIImage imageNamed:@"ProfileLockOn"];
    vm0.isImageFirst = NO;
    
    SJStaticTableviewSectionViewModel *section0 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm0]];
    
    

    // ========== section 1
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftTitle = @" New message notification";
    vm1.identifier = 1;
    
    // Add an additional switch
    SJStaticTableviewCellViewModel *vm7 = [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftTitle = @night Mode;
    vm7.switchValueDidChangeBlock = ^(BOOL isON){
        NSString *message = isON?@" Turn on night mode":@" Turn off night mode";
        NSLog(@ "% @",message);
    };
    vm7.staticCellType = SJStaticCellTypeSystemAccessorySwitch;
    vm7.identifier = 7;
    
    SJStaticTableviewCellViewModel *vm8 = [[SJStaticTableviewCellViewModel alloc] init];
    vm8.leftTitle = @" Clear cache";
    vm8.indicatorLeftTitle = @ "12.3 M";
    vm8.identifier = 8;
    
    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftTitle = @ "privacy";
    vm2.identifier = 2;
    
    
    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftTitle = @ "generic";
    vm3.identifier = 3;
    
    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1,vm7,vm8,vm2,vm3]];
    



    // ========== section 2
    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftTitle = @" Help and Feedback";
    vm4.identifier = 4;
    
    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftTitle = About wechat;
    vm5.identifier = 5;
    
    SJStaticTableviewSectionViewModel *section2 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm4,vm5]];
    


      // ========== section 4
    SJStaticTableviewCellViewModel *vm9 = [[SJStaticTableviewCellViewModel alloc] init];
    vm9.leftTitle = @" Custom Cell display page - Group";
    vm9.identifier = 9;
    
    SJStaticTableviewCellViewModel *vm10 = [[SJStaticTableviewCellViewModel alloc] init];
    vm10.leftTitle = @" Custom cell display page - Same group";
    vm10.identifier = 10;
    
    SJStaticTableviewSectionViewModel *section4 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm9,vm10]];
    
    

    // ========== section 3
    SJStaticTableviewCellViewModel *vm6 = [[SJStaticTableviewCellViewModel alloc] init];
    vm6.staticCellType = SJStaticCellTypeSystemLogout;
    vm6.cellID = @"logout";
    vm6.identifier = 6;
   
    SJStaticTableviewSectionViewModel *section3 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm6]];
    
    return @[section0,section1,section2,section4,section3];
}
Copy the code

We can see that the array given to the dataSource is a two-dimensional array:

  • The first dimension is the section array, where the elements are the viewModel for each section:SJStaticTableviewSectionViewModel.
  • The second dimension is the cell array, and the elements are the viewModel corresponding to each cell:SJStaticTableviewCellViewModel.

There are several SJStaticTableviewCellViewModel properties need to emphasize:

  1. IsImageFirst: Because there is an image and a label to the left of the arrow to the right of the first group of cells on the page, you need to set the order of the two. Since the default is the image next to the arrow, we need to reset it to NO to make the label next to the arrow.
  2. Identifier: This attribute is an integer that identifies each cell and is used to determine when the user clicks on the cell. I did not associate the user’s click with the index of the cell, because sometimes we may change the order of the cell or delete a cell, so it is improper to rely on the index of the cell, which is easy to make mistakes.
  3. CellID: This property is used for cell reuse. The layout of individual cells is always different: there is a cell for logging out, so it needs to be distinguished from other cells (cellID can be left unset and has a default value to mark the most commonly used cell type).

Obviously, the Factory class belongs to Model and gives the “pure data” to the two ViewModels used by the dataSource. This class is my own, and readers can customize it as they use the framework.

Now that we know how to set up the data source, let’s look at the third question:

Question 3: How are cell drawing methods distinguished?

In the dataSource cellForRow: method, I used the block method to draw the cell.

Let’s look at the block definition:

typedef void(^SJStaticCellConfigureBlock)(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel * viewModel);
Copy the code

This block calls back inside the controller to draw different cells by determining their type.

So how are the different types of cells differentiated? — I used categories.

The SJStaticTableViewCell must have a class to be classified

Take a look at its header:

// All cells are categories of this class

@interface SJStaticTableViewCell : UITableViewCell

@property (nonatomic.strong) SJStaticTableviewCellViewModel *viewModel;

/ / = = = = = = = = = = = = = = = cell of all controls system style = = = = = = = = = = = = = = = / /

// Left half
@property (nonatomic.strong) UIImageView *leftImageView;               // ImageView on the left
@property (nonatomic.strong) UILabel *leftTitleLabel;                  // Label on the left

// Right half
@property (nonatomic.strong) UIImageView *indicatorArrow;              // Arrow on the right
@property (nonatomic.strong) UIImageView *indicatorLeftImageView;      // Right arrow to the left of imageView
@property (nonatomic.strong) UILabel *indicatorLeftLabel;              // Label to the left of the arrow on the right
@property (nonatomic.strong) UISwitch *indicatorSwitch;                // Right arrow left switch
@property (nonatomic.strong) UILabel *logoutLabel;                     // Exit the logged label

/ / = = = = = = = = = = = = = = = users to customize the inside of the cell control = = = = = = = = = = = = = = = / /

// The avatar cell in the MeViewController
@property (nonatomic.strong) UIImageView *avatarImageView;
@property (nonatomic.strong) UIImageView *codeImageView;
@property (nonatomic.strong) UIImageView *avatarIndicatorImageView;
@property (nonatomic.strong) UILabel *userNameLabel;
@property (nonatomic.strong) UILabel *userIdLabel;


// Uniformly layout the contents of the left part of the cell (title/image + title). All system-style cells call this method
- (void)layoutLeftPartSubViewsWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;

@end
Copy the code

Here I define all the controls and a method for laying out the controls to the left of the cell. Because the left side of almost all categories is almost similar, it is extracted.

So how many categories are there? (See the enumerated types in the cellViewModel header above.)

// There is a cell on the right side of the head (most common)
@interface SJStaticTableViewCell (AccessoryDisclosureIndicator)
- (void)configureAccessoryDisclosureIndicatorCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
Copy the code
// There is no cell control on the right
@interface SJStaticTableViewCell (AccessoryNone)
- (void)configureAccessoryNoneCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
Copy the code
// On the right is the switch cell
@interface SJStaticTableViewCell (AccessorySwitch)
- (void)configureAccessorySwitchCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
Copy the code
// Exit the login cell
@interface SJStaticTableViewCell (Logout)
- (void)configureLogoutTableViewCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
Copy the code
// A custom cell (first row of personal page)
@interface SJStaticTableViewCell (MeAvatar)
- (void)configureMeAvatarTableViewCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
Copy the code

When using the framework, you can add your own categories if you encounter situations that do not meet your current requirements.

Question 4: What happened to the UITableViewDelegate method?

When it comes to UITableViewDelegate’s delegate methods, the one we’re most familiar with is didSelectRowAtIndexPath:.

But when I write in this framework, defines an inheritance in the UITableViewDelegate agent: SJStaticTableViewDelegate, and give it added a proxy methods: ` `

@protocol SJStaticTableViewDelegate <UITableViewDelegate>

@optional

- (void)didSelectViewModel: (SJStaticTableviewCellViewModel *)viewModel atIndexPath:(NSIndexPath *)indexPath;

@end
Copy the code

This method returns the viewModel of the currently clicked cell, weakening indexPath.

Why would you do that?

Think of the original proxy method for clicking the cell: didSelectRowAtIndexPath:. Using this click method, we get the indexPath corresponding to the cell, and then using this indexPath, we can look up the corresponding model (viewModel or Model) in the data source.

Therefore, the method I defined returns the viewModel of the clicked cell directly, saving the user a step. Of course you can use the system’s original didSelectRowAtIndexPath method if you want to use it.

Take a look at how this new proxy method is implemented:

//SJStaticTableView.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    if ((self.sjDelegate) && [self.sjDelegate respondsToSelector:@selector(didSelectViewModel:atIndexPath:)]) {
        
        SJStaticTableviewCellViewModel *cellViewModel = [self.sjDataSource tableView:tableView cellViewModelAtIndexPath:indexPath];
        [self.sjDelegate didSelectViewModel:cellViewModel atIndexPath:indexPath];
        
    }else if((self.sjDelegate)&& [self.sjDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]){
        
        [self.sjDelegate tableView:tableView didSelectRowAtIndexPath:indexPath]; }}Copy the code

Now that you have an idea of how to implement the framework, LET me talk about the customization of the framework.

4. Custom


The framework has a configuration file: sjconconst. H, which defines all the default data and configuration of the framework, such as the font and color of the lable on the left of the cell. Distance between label and image on the left; Font and color of the label on the right, default size of the image on the right, and so on. Take a look at the code:

#ifndef SJConst_h
#define SJConst_h

//distance
#define SJScreenWidth [UIScreen mainScreen].bounds.size.width
#define SJScreenHeight [UIScreen mainScreen].bounds.size.height

#define SJTopGap 8 //same as bottom gap
#define SJLeftGap 12 //same as right gap
#define SJLeftMiddleGap 10 //in left part: the gap between image and label
#define SJRightMiddleGap 6 //in right part: the gap between image and label
#define SJImgWidth 30 //default width and height
#define SJTitleWidthLimit 180 //limt width of left and right labels

//image
#define SJIndicatorArrow @"arrow"

//font
#define SJLeftTitleTextFont [UIFont systemFontOfSize:15]
#define SJLogoutButtonFont [UIFont systemFontOfSize:16]
#define SJIndicatorLeftTitleTextFont [UIFont systemFontOfSize:13]

//color
#define SJColorWithRGB(R,G,B,A) [UIColor colorWithRed:R/255.0 green:G/255.0 blue:B/255.0 alpha:A]
#define SJLeftTitleTextColor [UIColor blackColor]
# define SJIndicatorLeftTitleTextColor SJColorWithRGB (136136136, 1)

#endif /* SJConst_h */
Copy the code

The default configuration defined here is used when cellViewModel and sectionViewModel are initialized:

The cell viewModel:

//SJStaticTableviewCellViewModel.m
- (instancetype)init
{
    self = [super init];
    if (self) {        
        _cellHeight = 44;
        _cellID = @"defaultCell";
        _staticCellType = SJStaticCellTypeSystemAccessoryDisclosureIndicator;// The default is the cell with the triangle arrow
        _isImageFirst = YES;
        
        // These are the default configurations
        _leftLabelTextFont = SJLeftTitleTextFont;
        _leftLabelTextColor = SJLeftTitleTextColor;
        _leftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _leftImageAndLabelGap = SJLeftMiddleGap;
        _indicatorLeftLabelTextFont = SJIndicatorLeftTitleTextFont;
        _indicatorLeftLabelTextColor = SJIndicatorLeftTitleTextColor;
        _indicatorLeftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _indicatorLeftImageAndLabelGap = SJRightMiddleGap;
    }
    return self;
}
Copy the code

A section of the viewModel:

- (instancetype)initWithCellViewModelsArray:(NSArray *)cellViewModelsArray
{
    self = [super init];
    if (self) {
        _sectionHeaderHeight = 10;
        _sectionFooterHeight = 10;
        _leftLabelTextFont = SJLeftTitleTextFont;
        _leftLabelTextColor = SJLeftTitleTextColor;
        _leftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _leftImageAndLabelGap = SJLeftMiddleGap;
        _indicatorLeftLabelTextFont = SJIndicatorLeftTitleTextFont;
        _indicatorLeftLabelTextColor = SJIndicatorLeftTitleTextColor;
        _indicatorLeftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _indicatorLeftImageAndLabelGap = SJRightMiddleGap;
        _cellViewModelsArray = cellViewModelsArray;        
    }
    return self;
}
Copy the code

Obviously, there is only one set of default configurations, but it is possible for an app to have both a Settings page and a personal page. The style of the two pages may be different, so the default configuration can only be assigned to one of the pages, and the other page needs to be configured separately, thus allowing for customization.

Take another look at the diagram showing the customization effect:

Take a look at how the data sources for these two pages are set up:

Group page:

+ (NSArray *)customCellsPageData
{
    // Default configuration
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm1.leftTitle = @" All default Settings for comparison";
    vm1.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm1.indicatorLeftTitle = "King of Glory!";
    
    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1]];
    
    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm2.leftTitle = "Left image becomes smaller";
    vm2.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm2.indicatorLeftTitle = "King of Glory!";
    
    SJStaticTableviewSectionViewModel *section2 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm2]];
    section2.leftImageSize = CGSizeMake(20.20);
    
    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm3.leftTitle = @" Font becomes smaller and red";
    vm3.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm3.indicatorLeftTitle = "King of Glory!";
    
    SJStaticTableviewSectionViewModel *section3 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm3]];
    section3.leftLabelTextFont = [UIFont systemFontOfSize:8];
    section3.leftLabelTextColor = [UIColor redColor];
    
    
    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm4.leftTitle = @" The distance between the two controls on the left becomes larger";
    vm4.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm4.indicatorLeftTitle = "King of Glory!";
    
    SJStaticTableviewSectionViewModel *section4 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm4]];
    section4.leftImageAndLabelGap = 20;
    
    
    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm5.leftTitle = @" Picture on the right becomes smaller";
    vm5.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm5.indicatorLeftTitle = "King of Glory!";
    
    SJStaticTableviewSectionViewModel *section5 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm5]];
    section5.indicatorLeftImageSize = CGSizeMake(15.15);
    
    
    SJStaticTableviewCellViewModel *vm6= [[SJStaticTableviewCellViewModel alloc] init];
    vm6.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm6.leftTitle = @" The right side is bigger and blue";
    vm6.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm6.indicatorLeftTitle = "King of Glory!";
    
    SJStaticTableviewSectionViewModel *section6 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm6]];
    section6.indicatorLeftLabelTextFont = [UIFont systemFontOfSize:18];
    section6.indicatorLeftLabelTextColor = [UIColor blueColor];
    
    
    SJStaticTableviewCellViewModel *vm7= [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm7.leftTitle = @" The distance between the two controls on the right is larger";
    vm7.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm7.indicatorLeftTitle = "King of Glory!";
    
    SJStaticTableviewSectionViewModel *section7 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm7]];
    section7.indicatorLeftImageAndLabelGap = 18;
    
    
    return @[section1,section2,section3,section4,section5,section6,section7];
    
}
Copy the code

As you can see, the custom code works on the Section viewModel.

Group page:

+ (NSArray *)customCellsOneSectionPageData
{
    // Default configuration
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm1.leftTitle = @" All default Settings for comparison";
    vm1.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm1.indicatorLeftTitle = "King of Glory!";
    
    
    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm2.leftTitle = "Left image becomes smaller";
    vm2.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm2.indicatorLeftTitle = "King of Glory!";
    vm2.leftImageSize = CGSizeMake(20.20);
    
    
    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm3.leftTitle = @" Font becomes smaller and red";
    vm3.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm3.indicatorLeftTitle = "King of Glory!";
    vm3.leftLabelTextFont = [UIFont systemFontOfSize:8];
    vm3.leftLabelTextColor = [UIColor redColor];
    
    
    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm4.leftTitle = @" The distance between the two controls on the left becomes larger";
    vm4.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm4.indicatorLeftTitle = "King of Glory!";
    vm4.leftImageAndLabelGap = 20;
    
    
    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm5.leftTitle = @" Picture on the right becomes smaller";
    vm5.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm5.indicatorLeftTitle = "King of Glory!";
    vm5.indicatorLeftImageSize = CGSizeMake(15.15);
    
    
    SJStaticTableviewCellViewModel *vm6= [[SJStaticTableviewCellViewModel alloc] init];
    vm6.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm6.leftTitle = @" The right side is bigger and blue";
    vm6.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm6.indicatorLeftTitle = "King of Glory!";
    vm6.indicatorLeftLabelTextFont = [UIFont systemFontOfSize:18];
    vm6.indicatorLeftLabelTextColor = [UIColor blueColor];
    
    
    SJStaticTableviewCellViewModel *vm7= [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm7.leftTitle = @" The distance between the two controls on the right is larger";
    vm7.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm7.indicatorLeftTitle = "King of Glory!";
    vm7.indicatorLeftImageAndLabelGap = 18;
    
    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1,vm2,vm3,vm4,vm5,vm6,vm7]];
    
    return @[section1];
}
Copy the code

Group pages are customised and grouped consistently for comparison purposes. We can see that the custom code is applied to the viewModel of the cell.

Why group and group presentations?

The purpose of the group and group presentations is to demonstrate the two customizations of the framework.

  • Grouping pages demonstrate section-level customization: Cell configuration is handed over to the Section layer viewModel. Once set, all cells in the section retain this configuration.

  • The group page shows cell-level customization: The task of configuring the cell is handed over to the viewModel of the cell layer. Once set, only the current cell has this configuration, which does not affect other cells.

In order to save the trouble, only configure the viewModel of the section layer (it is not elegant to set the same configuration for each cell), because from a design point of view, it is rare to have different styles of cells in a section. For example, in a section, it is unlikely that the image sizes in the two cells will be different, or that the font sizes will be different.

Let’s look at the section-level custom code:

// Set the font of the left label in the group of all cells
- (void)setLeftLabelTextFont:(UIFont *)leftLabelTextFont
{
    if(_leftLabelTextFont ! = leftLabelTextFont) {if(! [self font1:_leftLabelTextFont hasSameFontSizeOfFont2:leftLabelTextFont]) {
            
            _leftLabelTextFont = leftLabelTextFont;
            
            // If the new width is larger than the old width, it needs to be reset, otherwise it does not need to be reset
            [_cellViewModelsArray enumerateObjectsUsingBlock:^(SJStaticTableviewCellViewModel * viewModel, NSUInteger idx, BOOL * _Nonnull stop) {
                viewModel.leftLabelTextFont = _leftLabelTextFont;
                CGSize size = [self sizeForTitle:viewModel.leftTitle withFont:_leftLabelTextFont];
                if(size.width > viewModel.leftTitleLabelSize.width) { viewModel.leftTitleLabelSize = size; }}]; }}}// Change the color of the left label word in the group of all cells
- (void)setLeftLabelTextColor:(UIColor *)leftLabelTextColor
{
    if(! [self color1:_leftLabelTextColor hasTheSameRGBAOfColor2:leftLabelTextColor]) {
         _leftLabelTextColor = leftLabelTextColor;
        [_cellViewModelsArray makeObjectsPerformSelector:@selector(setLeftLabelTextColor:) withObject:_leftLabelTextColor]; }}// Resize the left image in the group of all cells
- (void)setLeftImageSize:(CGSize)leftImageSize
{
    SJStaticTableviewCellViewModel *viewMoel = _cellViewModelsArray.firstObject;
    
    CGFloat cellHeight = viewMoel.cellHeight;
    if((!CGSizeEqualToSize(_leftImageSize, leftImageSize)) && (leftImageSize.height < cellHeight)) {
        _leftImageSize = leftImageSize;
        [_cellViewModelsArray enumerateObjectsUsingBlock:^(SJStaticTableviewCellViewModel *viewModel, NSUInteger idx, BOOL* _Nonnull stop) { viewMoel.leftImageSize = _leftImageSize; }]; }}Copy the code

Because each section holds the viewModels of all cells inside it, the set method needs to update the viewModels of all cells if the configuration passed in is inconsistent with the current configuration.

Why have a cell layer configuration when the section ViewModel can do this?

Just to increase configuration freedom, what if a requirement comes along that requires a cell to be unique? (You know what I mean by god ^^)

The set method of the viewModel property of the cell is implemented in the same way as the section method.

5. Added the refresh function

In version 1.1.2, the data source is refreshed after being updated. For example: a discovery page emulates a network request and updates a cell’s viewModel after the request ends:

// Simulate a network request
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        // Request successful x
        NSDictionary *responseDict = @{@"title_info":@" New game is on the shelves".@"title_icon":@"game_1".@"game_info":"Let's play doudizhu!".@"game_icon":@"doudizhu"
                                       };
        // Refresh cell indexPath
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:3];
        
        // Get the viewModel corresponding to the cell
        SJStaticTableviewCellViewModel *viewModel = [self.dataSource tableView:self.tableView cellViewModelAtIndexPath:indexPath];
        
        if (viewModel) {
            / / update the viewModel
            viewModel.leftTitle = responseDict[@"title_info"];
            viewModel.leftImage = [UIImage imageNamed:responseDict[@"title_icon"]];
            viewModel.indicatorLeftImage = [UIImage imageNamed:responseDict[@"game_icon"]];
            viewModel.indicatorLeftTitle = responseDict[@"game_info"];
            
            / / refresh tableview
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; }});Copy the code

Effect:

6. Added support for data sources to rely entirely on network requests

In version 1.2.0, the data source is fully dependent on network requests.

In the latest version, the SJStaticViewController is created in two cases:

  1. SJDefaultDataTypeExist: Data exists before the table is generated. It can be all data of the table or the default data of the table (later, some data is updated through network request, refer to the previous section).
  2. SJDefaultDataTypeNone: means that there is currently no default data available, that is, no tableView can be generated, you need to manually call the data source generation and table generation method after the network request gets the data.
//SJStaticTableViewController.h
typedef enum : NSUInteger {
    
    SJDefaultDataTypeExist,    // There is data before the table is generated (1. Complete data is available without network request 2. Then update the data and refresh the table through network request.)
    SJDefaultDataTypeNone,     // Can not generate default data, need to rely on network request completely, after receiving data, generate table
    
}SJDefaultDataType;

- (instancetype)initWithDefaultDataType:(SJDefaultDataType)defualtDataType;
Copy the code
//SJStaticTableViewController.m
- (instancetype)initWithDefaultDataType:(SJDefaultDataType)defualtDataType
{
    self = [super init];
    if (self) {
        self.defualtDataType = defualtDataType;
    }
    return self;
}

- (instancetype)init
{
    self = [self initWithDefaultDataType:SJDefaultDataTypeExist];// The default is SJDefaultDataTypeExist
    return self;
}

- (void)viewDidLoad {
    
    [super viewDidLoad];
    [self configureNav];
    
    // Construct a tableView if all or part of the data source is available to tableivew;
    // Otherwise, you need to manually call the configureTableView method after the network request ends
    if (self.defualtDataType == SJDefaultDataTypeExist) {
        [selfconfigureTableView]; }}// Only SJDefaultDataTypeExist will be called automatically, otherwise it needs to be called manually
- (void)configureTableView
{
    [self createDataSource];// Generate the data source
    [self createTableView];// Create a table
}
Copy the code

Looking at an example, we set the emoji page to SJDefaultDataTypeNone, which means we need to manually call the configureTableView method:

- (void)viewDidLoad {
    
    [super viewDidLoad];
    
     self.navigationItem.title = @ "expression";
    [self networkRequest];
}


- (void)networkRequest
{
    [MBProgressHUD showHUDAddedTo: self.view animated:YES];
    
    // Simulate a network request
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        [MBProgressHUD hideHUDForView: self.view animated:YES];
         self.modelsArray = [Factory emoticonPage];// After the network request, save the data in self.modelsarray
        [self configureTableView];// Call manually
        
    });
}

- (void)createDataSource
{
    self.dataSource = [[SJStaticTableViewDataSource alloc] initWithViewModelsArray:self.modelsArray configureBlock:^(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel *viewModel) {
        
        switch (viewModel.staticCellType) {
                
            case SJStaticCellTypeSystemAccessoryDisclosureIndicator:
            {
                [cell configureAccessoryDisclosureIndicatorCellWithViewModel:viewModel];
            }
                break;
                
            default:
                break; }}]; }Copy the code

Take a look at the effect:

I hope you can give us your valuable advice if you think something is wrong

This post has been synced to my blog: Portal

— — — — — — — — — — — — — — — — — — — — — — — — — — — — on July 17, 2018 update — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Pay attention!!

The author recently opened a personal public account, mainly to share programming, reading notes, thinking articles.

  • Programming articles: including selected technical articles published by the author before, and subsequent technical articles (mainly original), and gradually away from iOS content, will shift the focus to improve the direction of programming ability.
  • Reading notes: Share reading notes on programming, thinking, psychology, and career books.
  • Thinking article: to share the author’s thinking on technology and life.

Because the number of messages released by the official account has a limit, so far not all the selected articles in the past have been published on the official account, and will be released gradually.

And because of the various restrictions of the major blog platform, the back will also be released on the public number of some short and concise, to see the big dry goods article oh ~

Scan the qr code of the official account below and click follow, looking forward to growing with you