使用 UITableViewAutomaticDimension 行高自动滚动到表格底部? - 斯威夫特,iOS 8+

Posted

技术标签:

【中文标题】使用 UITableViewAutomaticDimension 行高自动滚动到表格底部? - 斯威夫特,iOS 8+【英文标题】:Automatically scrolling to the bottom of a table with UITableViewAutomaticDimension row height? - Swift, iOS 8+ 【发布时间】:2015-10-24 12:10:31 【问题描述】:

通常,在表格中,可以使用以下代码自动滚动到底部:

let indexPath = NSIndexPath(forRow: rowCount - 1, inSection: 0)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)

但是,当我将其设置为在 viewDidLoad() 中使用自动行高时,如下所示,上面的代码仅向下滚动大约一半(取决于行高和行数 - 它只发生在多行中远大于估计的RowHeight)。

tableView.estimatedRowHeight = 50
tableView.rowHeight = UITableViewAutomaticDimension

如何在使用 UITableViewAutomaticDimension 时以编程方式向下滚动到表格底部?

【问题讨论】:

我知道它是旧的,但你尝试过 contentSize 属性吗? 【参考方案1】:

在使用自动调整单元格大小的表格视图底部插入行时,我遇到了非常相似的问题,这是我能找到的唯一解决方法。它并不漂亮,但可以完成工作(在 ios 8 和 9 上测试)。 在插入时:

// compute the insertion index as the last one
let insertionIndex = NSIndexPath(forRow: elements.count - 1, inSection: 0)
// delete from the top, insert at the bottom
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths(deletionsIndexes, withRowAnimation: .Top)
tableView.insertRowsAtIndexPaths([insertionIndex], withRowAnimation: .Fade)
tableView.endUpdates()
// scroll to the last row, position will be wrong
tableView.scrollToRowAtIndexPath(lastIndex, atScrollPosition: .Bottom, animated: false)
// scroll back to the one before, the actual scrolling will be done after inserting the cell
if elements.count > 1 
     let prevIndex = NSIndexPath(forRow: lastIndex.row - 1, inSection: 0)
     self.tableView.scrollToRowAtIndexPath(prevIndex, atScrollPosition: .Bottom, animated: false)

如您所见,我们滚动到最后一行只是为了触发行插入 然后滚动回之前的行(全部没有动画)以避免任何可见的滚动。 然后在插入单元格时:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell 

    [...]

    let cell = try configuredCell(forElement: element)
    dispatch_async(dispatch_get_main_queue()) 
        let lastIndex = NSIndexPath(forRow: self.elements.count - 1, inSection: 0)
        // when inserting the last element we do the actual animated scrolling
        if indexPath.row == self.elements.count - 1 
            tableView.scrollToRowAtIndexPath(lastIndex, atScrollPosition: .Bottom, animated: true)
        
    
    return cell

【讨论】:

【参考方案2】:

我遇到了同样的问题,添加这样的延迟帮助我解决了这个问题:

let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))

dispatch_after(time, dispatch_get_main_queue(), 
    let indexPath = NSIndexPath(forRow: rowCount - 1, inSection: 0)
    self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
)

【讨论】:

我认为这是单元格尚未呈现的问题?我正在使用 50 行,即使有 10 秒的延迟,它仍然没有完全滚动到底部。 是的,似乎是渲染时间问题。它对你有用吗? 查看我编辑的评论 - 我在输入完之前不小心按了 Enter 我明白了。您能否尝试实现tableView:estimatedHeightForRowAtIndexPath: 并为您确定的单元格返回尽可能接近的高度。您可以在覆盖其余单元格的else 部分中返回UITableViewAutomaticDimension 。这可能是错误的单元格高度计算 - 只是一个想法。 我有一个表格视图,每个单元格都有一个标签,大约 50 高,大约 200+ 高。如果我使用estimatedHeightForRowAtIndexPath 并计算该函数中的行数,我可以获得更准确的数字,它确实滚动到底部,但代码不是很整洁 - 这是我唯一的选择(计算行数,乘以字体大小并添加填充)?【参考方案3】:

实际上,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

【讨论】:

以上是关于使用 UITableViewAutomaticDimension 行高自动滚动到表格底部? - 斯威夫特,iOS 8+的主要内容,如果未能解决你的问题,请参考以下文章

在 HeightOfRow 中使用自动调整大小

带有嵌入 UICollectionView 的 UITableView 不起作用

测试使用

第一篇 用于测试使用

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

今目标使用教程 今目标任务使用篇