实际上,UITableView 的这种“错误行为”的原因是行高估计。当使用动态单元格高度时,tableView.contentSize 和例如 [tableView rectForHeaderInSection: lastSection] 的结果对于尚未显示的项目(例如仅具有估计布局矩形的项目。例如,矩形部分可能位于 contentSize 之外或内部某处,即使内部没有单元格也是如此)。
此外,scollRectToVisible: 似乎只适用于有效的矩形。因此,以当前对表格视图项的错误估计来调用它可能只会导致任何结果......
我对这个问题的解决方案是一个 UITableView 类别,它只是迭代几次以滚动到所需的位置。所以 UITableView 有机会计算真正的项目布局矩形(页眉、单元格、页脚)。我的测试表明,在大多数情况下,只需要一个循环。
UITableView+LEAScrollToVisible.h
//
// UITableView+LEAScrollToVisible.h
//
// Copyright © 2016 LaborEtArs. All rights reserved.
//
#import <UIKit/UIKit.h>
/**
UITableView (LEAScrollToVisible)
*/
@interface UITableView (LEAScrollToVisible)
/*
scrollSectionHeaderToVisible:withCompletion_LEA:
*/
- (void)scrollSectionHeaderToVisible:(NSInteger)pSectionHeaderIndex
withCompletion_LEA:(void (^)(void))pCompletion;
/*
scrollRowToVisible:withCompletion_LEA:
*/
- (void)scrollRowToVisible:(NSIndexPath *)pRowIndexPath
withCompletion_LEA:(void (^)(void))pCompletion;
/*
scrollSectionFooterToVisible:withCompletion_LEA:
*/
- (void)scrollSectionFooterToVisible:(NSInteger)pSectionFooterIndex
withCompletion_LEA:(void (^)(void))pCompletion;
@end
UITableView+LEAScrollToVisible.m
//
// UITableView+LEAScrollToVisible.m
//
// Copyright © 2016 LaborEtArs. All rights reserved.
//
#import "UITableView+LEAScrollToVisible.h"
/**
UITableView (LEAScrollToVisible)
*/
@implementation UITableView (LEAScrollToVisible)
/*
scrollSectionHeaderToVisible:withCompletion_LEA:
*/
- (void)scrollSectionHeaderToVisible:(NSInteger)pSectionHeaderIndex
withCompletion_LEA:(void (^)(void))pCompletion {
//FLog;
NSAssert((pSectionHeaderIndex < self.numberOfSections), @"Invalid section header index: %li", (long int)pSectionHeaderIndex);
// visible part of the scroll view
CGRect visibleRect = CGRectMake((self.contentInset.left + self.contentOffset.x),
(self.contentInset.top + self.contentOffset.y),
(CGRectGetWidth(self.frame) - (self.contentInset.left + self.contentInset.right)),
(CGRectGetHeight(self.frame) - (self.contentInset.top + self.contentInset.bottom)));
// maximum content offset (to avoid to scroll too far)
CGFloat maxPossibleContentOffset = MAX((-self.contentInset.top),
(self.contentSize.height -
CGRectGetHeight(visibleRect) -
self.contentInset.top));
// the rect for the header view (maybe just estimated)
CGRect sectionHeaderRect = [self rectForHeaderInSection:pSectionHeaderIndex];
// the theoretical offset to show the header rect topmost in the table view visible area
CGFloat contentOffsetToBeVisibleAtTop = (CGRectGetMinY(sectionHeaderRect) - self.contentInset.top);
// the real target offset
CGFloat targetContentOffsetY = MIN(contentOffsetToBeVisibleAtTop, maxPossibleContentOffset);
if (0.5 <= fabs(self.contentOffset.y - targetContentOffsetY)) {
// current offset is different -> move
// disable user interaction to avoid disturbances
self.userInteractionEnabled = NO;
// Scroll to target offset
[self setContentOffset:CGPointMake(self.contentOffset.x, targetContentOffsetY)
animated:YES];
// Reiterate after waiting for the animation to finalize
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
// Reenable user interaction
self.userInteractionEnabled = YES;
[self scrollSectionHeaderToVisible:pSectionHeaderIndex
withCompletion_LEA:pCompletion];
});
} else {
// current offset fits -> execute completion block
if (pCompletion) {
pCompletion();
}
}
}
/*
scrollRowToVisible:withCompletion_LEA:
*/
- (void)scrollRowToVisible:(NSIndexPath *)pRowIndexPath
withCompletion_LEA:(void (^)(void))pCompletion {
//FLog;
NSAssert((pRowIndexPath.section < self.numberOfSections), @"Invalid index path: %@", pRowIndexPath);
NSAssert((pRowIndexPath.row < [self numberOfRowsInSection:pRowIndexPath.section]), @"Invalid index path: %@", pRowIndexPath);
// visible part of the scroll view
CGRect visibleRect = CGRectMake((self.contentInset.left + self.contentOffset.x),
(self.contentInset.top + self.contentOffset.y),
(CGRectGetWidth(self.frame) - (self.contentInset.left + self.contentInset.right)),
(CGRectGetHeight(self.frame) - (self.contentInset.top + self.contentInset.bottom)));
// maximum content offset (to avoid to scroll too far)
CGFloat maxPossibleContentOffset = MAX((-self.contentInset.top),
(self.contentSize.height -
CGRectGetHeight(visibleRect) -
self.contentInset.top));
// the rect for the cell view (maybe just estimated)
CGRect cellRect = [self rectForRowAtIndexPath:pRowIndexPath];
// the theoretical offset to show the row rect topmost in the table view visible area
CGFloat contentOffsetToBeVisibleAtTop = (CGRectGetMinY(cellRect) - self.contentInset.top);
// the real target offset
CGFloat targetContentOffsetY = MIN(contentOffsetToBeVisibleAtTop, maxPossibleContentOffset);
if (0.5 <= fabs(self.contentOffset.y - targetContentOffsetY)) {
// current offset is different -> move
// disable user interaction to avoid disturbances
self.userInteractionEnabled = NO;
// Scroll to target offset
[self setContentOffset:CGPointMake(self.contentOffset.x, targetContentOffsetY)
animated:YES];
// Reiterate after waiting for the animation to finalize
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
// Reenable user interaction
self.userInteractionEnabled = YES;
[self scrollRowToVisible:pRowIndexPath
withCompletion_LEA:pCompletion];
});
} else {
// current offset fits -> execute completion block
if (pCompletion) {
pCompletion();
}
}
}
/*
scrollSectionFooterToVisible:withCompletion_LEA:
*/
- (void)scrollSectionFooterToVisible:(NSInteger)pSectionFooterIndex
withCompletion_LEA:(void (^)(void))pCompletion {
//FLog;
NSAssert((pSectionFooterIndex < self.numberOfSections), @"Invalid section footer index: %li", (long int)pSectionFooterIndex);
// visible part of the scroll view
CGRect visibleRect = CGRectMake((self.contentInset.left + self.contentOffset.x),
(self.contentInset.top + self.contentOffset.y),
(CGRectGetWidth(self.frame) - (self.contentInset.left + self.contentInset.right)),
(CGRectGetHeight(self.frame) - (self.contentInset.top + self.contentInset.bottom)));
// maximum content offset (to avoid to scroll too far)
CGFloat maxPossibleContentOffset = MAX((-self.contentInset.top),
(self.contentSize.height -
CGRectGetHeight(visibleRect) -
self.contentInset.top));
// the rect for the footer view (maybe just estimated)
CGRect sectionFooterRect = [self rectForFooterInSection:pSectionFooterIndex];
// the theoretical offset to show the footer rect topmost in the table view visible area
CGFloat contentOffsetToBeVisibleAtTop = (CGRectGetMinY(sectionFooterRect) - self.contentInset.top);
// the real target offset
CGFloat targetContentOffsetY = MIN(contentOffsetToBeVisibleAtTop, maxPossibleContentOffset);
if (0.5 <= fabs(self.contentOffset.y - targetContentOffsetY)) {
// current offset is different -> move
// disable user interaction to avoid disturbances
self.userInteractionEnabled = NO;
// Scroll to target offset
[self setContentOffset:CGPointMake(self.contentOffset.x, targetContentOffsetY)
animated:YES];
// Reiterate after waiting for the animation to finalize
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
// Reenable user interaction
self.userInteractionEnabled = YES;
[self scrollSectionFooterToVisible:pSectionFooterIndex
withCompletion_LEA:pCompletion];
});
} else {
// current offset fits -> execute completion block
if (pCompletion) {
pCompletion();
}
}
}
@end