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 code
reloadData
The method will eventually call our customxy_reloadData
Methods. - we
xy_reloadData
Method, if you want to call the system’sreloadData
Method needs to be calledxy_reloadData
Methods.
+ (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
- use
layoutIfNeeded
methods - To obtain
The home side column
Asynchronous 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.