如何将UITableViewCell定位在屏幕底部?

27

在一个UITableView中,通常会有1到3个UITableViewCell。是否有一种方法,在reloadData后总是将单元格位置固定在屏幕底部?

+----------------+     +----------------+     +----------------+
|                |     |                |     |                |
|                |     |                |     |                |
|                |     |                |     |                |
|                |     |                |     | +------------+ |
|                |     |                |     | |   cell 1   | |
|                |     |                |     | +------------+ |
|                |     | +------------+ |     | +------------+ |
|                |     | |   cell 1   | |     | |   cell 2   | |
|                |     | +------------+ |     | +------------+ |
| +------------+ |     | +------------+ |     | +------------+ |
| |   cell 1   | |     | |   cell 2   | |     | |   cell 3   | |
| +------------+ |     | +------------+ |     | +------------+ |
+----------------+     +----------------+     +----------------+

滚动是否已启用? - Espresso
@Espresso 滚动不重要,只要最后一个单元格在底部。 - ohho
单元格的高度是否固定? - Nazik
@NAZIK 你可以假设是的。 - ohho
你可以把表格倒过来,然后旋转单元格来抵消这个问题。 - keji
14个回答

13

由于之前的答案已经不适用于现代使用,我创建了一个新的示例解决方案。

最新技术使用自动布局和自动调整大小的单元格,因此之前的答案将不再适用。我重新设计了解决方案以适应现代功能,并创建了一个示例项目放在GitHub上。

这段代码不再通过计算每行高度来累加高度,而是获取最后一行的框架,以便计算从顶部的内容插图。它利用了表视图已经在做的事情,因此不需要额外的工作。

此代码还仅在设置了用于键盘或其他覆盖层的底部插图时,才设置顶部插图。

请在GitHub上报告任何错误或提交改进意见,我会更新这个示例。

GitHub: https://github.com/brennanMKE/BottomTable

- (void)updateContentInsetForTableView:(UITableView *)tableView animated:(BOOL)animated {
    NSUInteger lastRow = [self tableView:tableView numberOfRowsInSection:0];
    NSUInteger lastIndex = lastRow > 0 ? lastRow - 1 : 0;

    NSIndexPath *lastIndexPath = [NSIndexPath indexPathForItem:lastIndex inSection:0];
    CGRect lastCellFrame = [self.tableView rectForRowAtIndexPath:lastIndexPath];

    // top inset = table view height - top position of last cell - last cell height
    CGFloat topInset = MAX(CGRectGetHeight(self.tableView.frame) - lastCellFrame.origin.y - CGRectGetHeight(lastCellFrame), 0);

    UIEdgeInsets contentInset = tableView.contentInset;
    contentInset.top = topInset;

    UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
    [UIView animateWithDuration:animated ? 0.25 : 0.0 delay:0.0 options:options animations:^{
        tableView.contentInset = contentInset;
    } completion:^(BOOL finished) {
    }];
}

为什么要传递tableView参数而不使用它? - HotJard
1
tableView.contentInset = contentInset; - BallpointBen

10

每当添加一行时,请调用此方法:

- (void)updateContentInset {
    NSInteger numRows=[self tableView:_tableView numberOfRowsInSection:0];
    CGFloat contentInsetTop=_tableView.bounds.size.height;
    for (int i=0;i<numRows;i++) {
        contentInsetTop-=[self tableView:_tableView heightForRowAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
        if (contentInsetTop<=0) {
            contentInsetTop=0;
            break;
        }
    }
    _tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0);
}

1
比起将一切都旋转来说,这种方式更加合适。 - lukasz
我会做的一个更改是,取当前内容插入值并仅修改顶部值。如果您正在显示键盘并且已将偏移量设置为允许底部内容显示,则可能需要将底部值设置为另一个值而不是0。 - Brennan
4
更简洁的方法是:self.tableView.contentInset = UIEdgeInsetsMake(MAX(0.0, self.tableView.bounds.size.height - self.tableView.contentSize.height), 0, 0, 0);。表格视图的内容大小是所有单元格的大小之和。 - JoJo

5

您可以在表格视图中设置标题并使其足够高以将第一个单元格向下推。然后相应地设置tableView的contentOffset。不过,我认为没有快速内置的方法来完成此操作。


@ohho 另外,您可以将表格分组。 - Mike D
我发布了一个新的答案,其中包含适用于自动布局和自动调整大小单元格的现代解决方案。请查看并投票支持它。 - Brennan

3

我不喜欢基于空单元格、contentInsettransform的解决方案,相反我想出了其他的解决方案:

Example

UITableView的布局是私有的,如果苹果公司需要,可能会发生变化。因此,最好拥有完全控制,从而使您的代码具备未来性和更高的灵活性。我转而使用UICollectionView,并基于UICollectionViewFlowLayout实现了特殊的布局(Swift 3):

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    // Do we need to stick cells to the bottom or not
    var shiftDownNeeded = false

    // Size of all cells without modifications
    let allContentSize = super.collectionViewContentSize()

    // If there are not enough cells to fill collection view vertically we shift them down
    let diff = self.collectionView!.bounds.size.height - allContentSize.height
    if Double(diff) > DBL_EPSILON {
        shiftDownNeeded = true
    }

    // Ask for common attributes
    let attributes = super.layoutAttributesForElements(in: rect)

    if let attributes = attributes {
        if shiftDownNeeded {
            for element in attributes {
                let frame = element.frame;
                // shift all the cells down by the difference of heights
                element.frame = frame.offsetBy(dx: 0, dy: diff);
            }
        }
    }

    return attributes;
}

它对我的用例非常有效,并且显然可以通过缓存内容尺寸高度来进行优化。此外,我不确定在大数据集上进行优化测试的性能如何,我没有测试过。我已经准备了一个带有演示的样品项目:MDBottomSnappingCells
这是Objective-C版本:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
{
    // Do we need to stick cells to the bottom or not
    BOOL shiftDownNeeded = NO;

    // Size of all cells without modifications
    CGSize allContentSize = [super collectionViewContentSize];

    // If there are not enough cells to fill collection view vertically we shift them down
    CGFloat diff = self.collectionView.bounds.size.height - allContentSize.height;
    if(diff > DBL_EPSILON)
    {
        shiftDownNeeded = YES;
    }

    // Ask for common attributes
    NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
    if(shiftDownNeeded)
    {
        for(UICollectionViewLayoutAttributes *element in attributes)
        {
            CGRect frame = element.frame;
            // shift all the cells down by the difference of heights
            element.frame = CGRectOffset(frame, 0, diff);
        }
    }

    return attributes;
}

1
我喜欢你的方法。但是,我有一个问题。在集合视图中如何使用自动布局获取动态单元格高度?我们可以在UITableView中使用UITableViewAutomaticDimension实现这一点。 - Nitesh Borad
我建议不要在UITableView/UICollectionView的单元格内使用自动布局,但如果您仍需要这样做,可以通过多种方式实现。对于集合视图,我发现了这个答案:https://dev59.com/v18e5IYBdhLWcg3wS425。 通常,对于UITableView,您会有一个静态的“大小调整单元格”,您可以使用您的数据进行配置并计算高度;对于集合视图,它工作方式不同,但是使用我建议的布局,您可以使用相同的概念。 - MANIAK_dobrii

2
可以使用以下函数在Swift中完成此操作。
func updateContentInsetForTableView(tblView: UITableView, animated: Bool) {

    let lastRow: NSInteger = self.tableView(tblView, numberOfRowsInSection: 0)
    let lastIndex: NSInteger = lastRow > 0 ? lastRow - 1 : 0
    let lastIndexPath: NSIndexPath = NSIndexPath(forRow: lastIndex, inSection: 0)
    let lastCellFrame: CGRect = tblView.rectForRowAtIndexPath(lastIndexPath)
    let topInset: CGFloat = max(CGRectGetHeight(tblView.frame) - lastCellFrame.origin.y - CGRectGetHeight(lastCellFrame), 0)
    var contentInset: UIEdgeInsets = tblView.contentInset
    contentInset.top = topInset
    let option: UIViewAnimationOptions = UIViewAnimationOptions.BeginFromCurrentState
    UIView.animateWithDuration(animated ? 0.25 : 0.0, delay: 0.0, options: option, animations: { () -> Void in
        tblView.contentInset = contentInset
    }) { (_) -> Void in }
}

2
这是@Brennan的解决方案的Swift 3版本,已经测试并通过了 :)
func updateContentInsetForTableView( tableView:UITableView,animated:Bool) {

    let lastRow = tableView.numberOfRows(inSection: 0)
    let lastIndex = lastRow > 0 ? lastRow - 1 : 0;

    let lastIndexPath = IndexPath(row: lastIndex, section: 9)


    let lastCellFrame = tableView.rectForRow(at: lastIndexPath)
    let topInset = max(tableView.frame.height - lastCellFrame.origin.y - lastCellFrame.height, 0)

    var contentInset = tableView.contentInset;
    contentInset.top = topInset;

    _ = UIViewAnimationOptions.beginFromCurrentState;
        UIView.animate(withDuration: 0.1, animations: { () -> Void in

    tableView.contentInset = contentInset;
   })

   } 

1

使用这个。肯定会有帮助。

- (void)reloadData
{
    [super reloadData];

    [self recalculateContentInset];
    [self recalculateScrollIndicator];
}

- (void)recalculateContentInset
{
    CGFloat contentInsetHeight = MAX(self.frame.size.height - self.contentSize.height, 0);
    CGFloat duration = 0.0;
    [UIView animateWithDuration:duration
                          delay:0
                        options:UIViewAnimationOptionCurveEaseOut
                     animations:^{
        [self setContentInset:UIEdgeInsetsMake(contentInsetHeight, 0, 0, 0)];
    }completion:nil];
}

- (void)recalculateScrollIndicator
{
    if(self.contentSize.height >= self.frame.size.height){
        [self setShowsVerticalScrollIndicator:YES];
    } else {
        [self setShowsVerticalScrollIndicator:NO];
    }
}

你能在你的答案中加入代码解释或至少一些注释吗? - RevanProdigalKnight

0
func updateContentInsetForTableView( tableView:UITableView,animated:Bool) {

    let lastRow = tableView.numberOfRows(inSection: 0)
    let lastIndex = lastRow > 0 ? lastRow - 1 : 0;

    let lastIndexPath = IndexPath(row: lastIndex, section: 9)

    let lastCellFrame = tableView.rectForRow(at: lastIndexPath)
    let topInset = max(tableView.frame.height - lastCellFrame.origin.y - lastCellFrame.height, 0)

    var contentInset = tableView.contentInset;
    contentInset.top = topInset;

    _ = UIViewAnimationOptions.beginFromCurrentState;
    UIView.animate(withDuration: 0.1, animations: { () -> Void in

        tableView.contentInset = contentInset;
    })


    if self.commesnts.count > 0 {
        tableView.scrollToRow(at: IndexPath(item:self.commesnts.count-1, section: 0), at: .bottom, animated: true)
    }

}

我使用了@bkokot的解决方案,并进行了一些小的添加。它有两个作用。

1. Start showing cells from bottom of UITableView
2. Scroll cells to bottom so that last inserted row become visible
(just like chat)

0

所有答案都有一些关于动态行高和/或动画的怪癖。对我来说,最好的解决方案是对表格进行转换(flipY):

tableView.transform = CGAffineTransform (scaleX: 1,y: -1)

cellForRowAt 中:

cell.contentView.transform = CGAffineTransform (scaleX: 1,y: -1)
cell.accessoryView?.transform = CGAffineTransform (scaleX: 1,y: -1)

您还可以反转数据数组,也可以翻转节头/页脚。 同时,您的页脚将变成新的页眉 - 但是,它确实有效。


0

我会根据单元格的数量调整UITableView在其父视图中的大小和位置。我想这是涉及最少变通方法的解决方案。 另外,你真的需要使用UITableView吗?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接