In front of the

At present, the project functions are almost done. Need to be refined and polished, today needs to show a friendly prompt view for all TableView list pages when there is no data, it is too cumbersome to change one by one. And the business logic is not easy to change. So I took some time to write a little thing. In the project, AN was used in accordance with the standard prefix of the project, and the prefix was changed to XY according to my own preference.

Demo

International practice, Demo first

advantages

  • Drag and drop to use, without import, without any modifications to the original code
  • You can also choose the implementation method for quick customization and complete customization

Thank you

Today I found this article, which is also the source of my thinking. What does the user say when UITableView has no data

The principle of

No invasion

Use the Runtime exchange method to achieve no intrusion on the original code. Create a TableView category in.m

#import <objc/runtime.h>
Copy the code

The current idea I have is to implement it in reloadData, so define a xy_reloadData method and then swap it with the original reloadData method.

In other words:

  • All calls in the codereloadDataThe method will eventually call our customxy_reloadDataMethods.
  • wexy_reloadDataMethod, if you want to call the system’sreloadDataMethod needs to be calledxy_reloadDataMethods.
+ (void)load {
    
    Method reloadData    = class_getInstanceMethod(self.@selector(reloadData));
    Method xy_reloadData = class_getInstanceMethod(self.@selector(xy_reloadData));
    method_exchangeImplementations(reloadData, xy_reloadData);
}
Copy the code

The description of the load method is

Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading. When a class or category is added to the Objective-C Runtime; Implement this method to perform class-specific behavior after loading.

So it can be loaded without import.

Get the amount of data in the TableView

TableView may have multiple Sections and each Section may have many cells. So you can’t just tell if the first Section has data. So let’s:

  • Gets the number of sections
  • Gets the number of cells in each Section
    NSInteger numberOfSections = [self numberOfSections];
    BOOL havingData = NO;
    for (NSInteger i = 0; i < numberOfSections; i++) {
        if ([self numberOfRowsInSection:i] > 0) {
            havingData = YES;
            break; }}Copy the code

In this case, the Boolean value havingData is a flag indicating whether there is data.

How to implement reloadData to fetch quantity after completion.

Because the reloadData method of TableView is implemented asynchronously. There are two ways to get to the state where the load is complete

  1. uselayoutIfNeededmethods
  2. To obtainThe home side columnAsynchronous execution

The code of the first method is:

    [self xy_reloadData];
    [self layoutIfNeeded];
    // Next code
Copy the code

We don’t want reloadData in the original business code to block until the code is loaded.

So I choose the second option

    [self xy_reloadData];
    dispatch_async(dispatch_get_main_queue(), ^{
        // Next code
    });
Copy the code

So our method in xy_reloadData is implemented as:

- (void)xy_reloadData {
    
    [self xy_reloadData];
    
    // Check the amount of data after the refresh
    dispatch_async(dispatch_get_main_queue(), ^{
        
        NSInteger numberOfSections = [self numberOfSections];
        BOOL havingData = NO;
        for (NSInteger i = 0; i < numberOfSections; i++) {
            if ([self numberOfRowsInSection:i] > 0) {
                havingData = YES;
                break; }} [self xy_havingData:havingData];
    });
}
Copy the code

Display a placeholder view

TableView has a backgroundView property that does a good job of that and can be assigned based on the state of havingData

- (void)xy_havingData:(BOOL)havingData {
    if (havingData) {
        self.backgroundView = nil;
    } else {
        selfBackgroundView = Custom view; }}Copy the code

How do I get my controller to customize the view

Of course, we are not satisfied with a simple view, we want the corresponding controller to be able to customize their own view according to their own needs.

The most common approach is of course to handle some of the TableView logic in the TableView’s proxy class (usually the controller)

So suppose we want the proxy class to implement a method xy_noDataView

    if ([self.delegate respondsToSelector:@selector(xy_noDataView)]) {
        self.backgroundView = [self.delegate performSelector:@selector(xy_noDataView)];
        return ;
    }
Copy the code

There will be a compile warning, which I chose to eliminate by defining a protocol in the.m file. I also defined some other methods to better fulfill my requirements.

/** Remove warning */
@protocol XYTableViewDelegate <NSObject>
@optional
- (UIView   *)xy_noDataView;                // Fully custom placeholder map
- (UIImage  *)xy_noDataViewImage;           // Use the default placeholder map, provide a picture, do not provide, default do not display
- (NSString *)xy_noDataViewMessage;         // Use the default placeholder map to provide the display text, the default is no data
- (UIColor  *)xy_noDataViewMessageColor;    // Use the default placeholder map, which provides the color of the displayed text
- (NSNumber *)xy_noDataViewCenterYOffset;   // Use the default placeholder map, CenterY downward offset
@end
Copy the code

The reason why we didn’t declare it in.h, and then ask the controller to implement our agent, and then to implement the method is to try to be as non-invasive as possible, programming by contract, implementation by rules can be effective.

I want to be able to drag and use it and throw it away

I also implemented some simple features. See the Demo for details.

The complete xy_havingData method is as follows:

- (void)xy_havingData:(BOOL)havingData {
    
    // No need to display a placeholder map
    if (havingData) {
        self.backgroundView = nil;
        return ;
    }
    
    // There is no need to duplicate the creation
    if (self.backgroundView) {
        return ;
    }
    
    // Custom placeholder map
    if ([self.delegate respondsToSelector:@selector(xy_noDataView)]) {
        self.backgroundView = [self.delegate performSelector:@selector(xy_noDataView)];
        return ;
    }
    
    // Use built-in
    UIImage  *img   = nil;
    NSString *msg   = @" No data yet";
    UIColor  *color = [UIColor lightGrayColor];
    CGFloat  offset = 0;
    
    // Get the image
    if ([self.delegate    respondsToSelector:@selector(xy_noDataViewImage)]) {
        img = [self.delegate performSelector:@selector(xy_noDataViewImage)];
    }
    // Get the text
    if ([self.delegate    respondsToSelector:@selector(xy_noDataViewMessage)]) {
        msg = [self.delegate performSelector:@selector(xy_noDataViewMessage)];
    }
    // Get the color
    if ([self.delegate      respondsToSelector:@selector(xy_noDataViewMessageColor)]) {
        color = [self.delegate performSelector:@selector(xy_noDataViewMessageColor)];
    }
    // Get the offset
    if ([self.delegate        respondsToSelector:@selector(xy_noDataViewCenterYOffset)]) {
        offset = [[self.delegate performSelector:@selector(xy_noDataViewCenterYOffset)] floatValue];
    }
    
    // Create a placeholder map
    self.backgroundView = [self xy_defaultNoDataViewWithImage  :img message:msg color:color offsetY:offset];
}
Copy the code

You can customize the View completely by using the custom View method. You can also use some of your own styles to specify the image, text, text color, and position offset. Of course, any one of them can not be specified, use the default Settings.

Some code for the interface

/** Default placeholder */
- (UIView *)xy_defaultNoDataViewWithImage:(UIImage *)image message:(NSString *)message color:(UIColor *)color offsetY:(CGFloat)offset {
    
    // Calculate the position, vertical center, image default center.
    CGFloat sW = self.bounds.size.width;
    CGFloat cX = sW / 2;
    CGFloat cY = self.bounds.size.height * (1 - 0.618) + offset;
    CGFloat iW = image.size.width;
    CGFloat iH = image.size.height;
    
    / / picture
    UIImageView *imgView = [[UIImageView alloc] init];
    imgView.frame        = CGRectMake(cX - iW / 2, cY - iH / 2, iW, iH);
    imgView.image        = image;
    
    / / text
    UILabel *label       = [[UILabel alloc] init];
    label.font           = [UIFont systemFontOfSize:17];
    label.textColor      = color;
    label.text           = message;
    label.textAlignment  = NSTextAlignmentCenter;
    label.frame          = CGRectMake(0.CGRectGetMaxY(imgView.frame) + 24, sW, label.font.lineHeight);
    
    / / view
    XYNoDataView *view   = [[XYNoDataView alloc] init];
    [view addSubview:imgView];
    [view addSubview:label];
    
    // Implement scrolling with the TableView
    [view addObserver:self forKeyPath:kXYNoDataViewObserveKeyPath options:NSKeyValueObservingOptionNew context:nil];
    return view;
}
Copy the code

Details of the optimization

How to load a page without displaying a placeholder map

When TableView is displayed on the interface, it is equivalent to calling the reloadData method. Therefore, according to our current logic, we will first show a placeholder map, and then call the reloadData method again to hide the placeholder map after data loading is completed.

We don’t want to display a placeholder map with no data before data is loaded, because there is likely to be data, so we can ignore the first call to reloadData and add the following validation to xy_reloadData: [self xy_reloadData]; After that, if the data is not loaded, we will process it as if there is data by default, that is, the placeholder map will not be displayed. Then record that the data has been loaded.

    // Ignore the first load
    if(! [self isInitFinish]) {
        [self xy_havingData:YES];
        [self setIsInitFinish:YES];
        return ;
    }
Copy the code

Bind a property to the TableView that records if it’s finished loading

/** The setup has finished loading data */
- (void)setIsInitFinish:(BOOL)finish {
    objc_setAssociatedObject(self.@selector(isInitFinish), @(finish), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/** Whether the data has been loaded */
- (BOOL)isInitFinish {
    id obj = objc_getAssociatedObject(self, _cmd);
    return [obj boolValue];
}
Copy the code

How to scroll a placeholder map to follow the TableView as it scrolls.

Because our placeholder map is assigned to the backgroundView property of the TableView, it is added to the TableView. As you can see from debugging, as the TableView rolls around contentOffset, BackgroundView’s frame.origin. Y also changes synchronously, so it looks like no matter how much the TableView scrolls the placeholder, if we want the placeholder to scroll with it, Just unsynchronize the frame.origin. Y of the backgroundView, which means that the value of frame.Origin is always 0.

I have not found a better way, temporarily use KVO to implement, remember to remove the KVO listening when View destruction, detailed implementation can see Demo.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    if ([keyPath isEqualToString:kXYNoDataViewObserveKeyPath]) {
        
        /** backgroundView's frame.origination. Y changes synchronously when TableView scrolling ContentOffset changes. But we want the backgroundView to scroll along with the TableView, so we can only force frame.origine.y to always be 0 compatible with MJRefresh */
        CGRect frame = [[change objectForKey:NSKeyValueChangeNewKey] CGRectValue];
        if(frame.origin.y ! =0) {
            frame.origin.y  = 0;
            self.backgroundView.frame = frame; }}}Copy the code

What if you don’t want to display a placeholder map?

Implement the following method on the corresponding controller

- (NSString *)xy_noDataViewMessage {
    return @"";
}
Copy the code

About secant lines

In the article I mentioned above. When modifying backgroundView property, modify the separatorStyle property of TableView. When there is no data, cancel the dividing line. When there is data, add it. But I used in the project of the TableView separatorStyle line is different. So I didn’t change the splitter property, but if you want to hide the splitter when there’s no data in the TableView, you can look at my Demo and add this line of code to the corresponding controller.

    self.tableView.tableFooterView = [UIView new];
Copy the code

The last

The numberOfRowsInSection method, which gets the number of cells in each Section, is numberOfItemsInSection.

Rookie a, if there is a great god not stingy to give advice, will be grateful.