Let him storm, I am still!
When we insert data into the UITableView in real time and refresh the list, we see that the list is jitter. For example, on a wechat chat page, you swipe to a certain location and hold it, and then you receive a wechat message from one or more people (who are not on the current chat list). You’ll notice that every time you receive a message from someone, the list sinks and there is a “shake” process. Of course, this is not to say that the wechat experience is not good, just to throw stones.
Without further ado, the scenario I’m going to discuss is as follows:
The current list shows a lot of news, while third-party ads are loading in the background. After the AD is loaded, it needs to be inserted into the list in a circular order, such as 5,12,19,26… , requires that the page displayed after the insertion of the advertisement does not sink and shake, so as to avoid the news that you have just seen jumping to an unknown location.
Since ads are not appended directly to the end of the list or inserted into adjacent places at once, they are distributed discretely throughout the list, So bad with insertRowsAtIndexPaths: withRowAnimation: or reloadRowsAtIndexPaths: withRowAnimation: local refresh, ReloadData must be for the whole list. This obviously causes the list to sink and wobble, and in the worst case, the entire page currently displayed sinks, which is a bad experience for the news client.
First of all, I would think: scrollToRowAtIndexPath atScrollPosition: animated: this method. After I refresh the entire list, I scroll the UITableView back to where I recorded it. Look at the code for a general idea:
// Find the news Id at the top of the current screen before refreshing the list
- (NSString *)topNewsId {
NSArray *visibleCells = [self.tableView visibleCells];
UITableViewCell *cell = [visibleCells firstObject];
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
NewsModel *topNews = [self.dataArr objectAtIndex:indexPath.row];
NSString *newsId = = topNews.newsId;
return newsId;
}
// After refreshing, scroll the news from the previous top to the top to avoid page jitter
- (void)keepTopNews:(NSString *)topNewsId {
int topNewsRow = 0;
for (int i = 0; i <[self.dataArr count] ; i ++) {
id data = [self.dataArr objectAtIndex:i];
if ([data isKindOfClass:[NewsModel class]]) {
NewsModel *model = data;
if ([model.newsId isEqualToString:topNewsId]) {
topNewsRow = i;
break;
}
}
}
if (topNewsRow) {
NSIndexPath *toIndex = [NSIndexPath indexPathForRow:topNewsRow inSection:0];
[self.tableView scrollToRowAtIndexPath:toIndex atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}Copy the code
At first glance, this approach looks elegant and seems to serve our purpose. But there’s a problem, and the problem is visibleCells. Let’s look at the definition of this method:
Returns an array of visible cells currently displayed by the collection view.
Returns the currently displayed visible array of cells. However, this method is not “seeing is believing”. Sometimes cells that are invisible to the naked eye are considered visible, or only partially visible, and are returned to us. For example, the news at the top of Netease news in the picture “… Lady lens of the Republic of China “only a part of it, if you use it for the top will also have the problem of subsidence shake.
So is there a more elegant way? Absolutely!!!
Since scrolling in cells is too crude, we can use pixel-level scrolling to elegantly keep the top stories intact.
First, we need to know one feature of ReloadData:
When you call this method, the collection view discards any currently visible items and views and redisplays them. For efficiency, the collection view displays only the items and supplementary views that are visible after reloading the data. If the collection view’s size changes as a result of reloading the data, the collection view adjusts its scrolling offsets accordingly.
The differences between ContentOffset, ContentSize, and ContentInset are unnecessary here.
Say ReloadData refresh the current screen only visible what cell, will only to visibleCells calls tableView: cellForRowAtIndexPath:. ContentOffset remains constant, so we see a “wobble” phenomenon, as news is squeezed out.
The gray part represents the iPhone screen, the pink part represents the layout size of all data, the white cell is the data hidden at the top of the screen, and the green part represents the targeted AD alone.
The news at the top of the current screen on the left is News 11. UITableview’s contentOffset is 200. We can calculate the sum of the heights of all the news cells before News 11 to give the current News 11 offset, preOffset.
On the right is the layout after inserting an AD in the third position. ContentOffset at UITableview is still 200, but News 11 has been “squeezed down”. We can also calculate the sum of the heights of all news cells and advertising cells before News 11 to obtain the afterOffset of news 11 now.
With preOffset and afterOffset, you can see how far news 11 was pushed down
deltaOffset = afterOffset – preOffset;
So, to keep news 11 displayed where it was, all we have to do is manually update the value of ContentOffset, which is equivalent to moving the pink part up by the distance of deltaOffset.
Look at the code:
- (void)insertAds:(NSArray *)ads {
NSString *topNewsId = [self topNewsId];
CGFloat preOffset = [self offSetOfTopNews:topNewsId];
/ *
Insert advertisement...
* /
[self.tableView reloadData];
CGFloat afterOffset = [self offSetOfTopNews:topNewsId];
CGFloat deltaOffset = afterOffset - preOffset;
CGPoint contentOffet = [self.tableView contentOffset];
contentOffet.y += deltaOffset;
self.tableView .contentOffset = contentOffet;
}
// Calculate the offset of newsId for the news
- (CGFloat)offSetOfTopNews:(NSString *)newsId {
CGFloat offset = 0;
for (int i = 0; i < [self.dataArr count]; i ++) {
id data = [self.dataArr objectAtIndex:i];
if ([data isKindOfClass:[NewsModel class]]) {
NewsModel *model = data;
if ([model.newsId isEqualToString:newsId]) {
break;
}
}
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
CGFloat height = [self heightForRowAtIndexPath:indexPath];
offset += height;
}
return offset;
}Copy the code
This way, the current screen really doesn’t sink at all. If the AD is inserted outside the current screen, the user will not notice it until the sliding list shows the AD in the corresponding position. If inserted into the current screen, the user sees a news inserted in the break area, but the top news position remains unchanged.
Enjoy the silky smooth ~
Finally, there is a little bit of a trick to calculating offsets.
If the height of all news and advertising units is fixed, then heightForRowAtIndexPath: is easy to calculate. If it’s dynamic, it’s a little tricky.
For example, advertising data is represented by AdModel. In order to dynamically adjust the height of the AD unit with the content of the AD, we usually use a cellHeight field in the AdModel.
@interface AdModel:NSObject
@property (nonatomic, assign) NSInteger adId;
.
@property (nonatomic, assign) CGFloat cellHeight;
@endCopy the code
Calculate the height and assign it to cellHeight as we fill in the content and render the AD space.
In the above scenario, the UITableView does not refresh the invisible AD space when ReloadData is inserted, so cellHeight is always 0, which results in heightForRowAtIndexPath: not being able to calculate the correct result.
Cleverly, we define a temporary AD unit variable AdCell when the AD inserts self.dataarr and actively call the render interface to assign cellHeight.
AdCell *tmpCell = [AdCell new];
[tmpCell setAdsContent:model]; // This renders the AD space and calculates cellHeightCopy the code