26

There are one to three UITableViewCells in a UITableViewView. Is there a way to always position the cell(s) at the bottom of screen after reloadData?

+----------------+     +----------------+     +----------------+
|                |     |                |     |                |
|                |     |                |     |                |
|                |     |                |     |                |
|                |     |                |     | +------------+ |
|                |     |                |     | |   cell 1   | |
|                |     |                |     | +------------+ |
|                |     | +------------+ |     | +------------+ |
|                |     | |   cell 1   | |     | |   cell 2   | |
|                |     | +------------+ |     | +------------+ |
| +------------+ |     | +------------+ |     | +------------+ |
| |   cell 1   | |     | |   cell 2   | |     | |   cell 3   | |
| +------------+ |     | +------------+ |     | +------------+ |
+----------------+     +----------------+     +----------------+
ohho
  • 47,708
  • 70
  • 236
  • 372

14 Answers14

13

I've create a new sample solution since the previous answer is not outdated for modern use.

The latest technique uses autolayout and self-sizing cells so the previous answer would no longer work. I reworked the solution to work with the modern features and created a sample project to put on GitHub.

Instead of counting up the height of each row, which is causes additional work, this code instead gets the frame for the last row so that the content inset from the top can be calculated. It leverages what the table view is already doing so no additional work is necessary.

This code also only sets the top inset in case the bottom inset is set for the keyboard or another overlay.

Please report any bugs or submit improvements on GitHub and I will update this sample.

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) {
    }];
}
Brennan
  • 11,067
  • 15
  • 61
  • 85
10

Call this method whenever a row is added:

- (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);
}
Florian L.
  • 777
  • 1
  • 7
  • 19
  • 1
    way much proper than screwing everything with rotation. – lukas Nov 17 '14 at 23:34
  • One change I would make is to instead take the current content inset value and modify just the top value. You may need the bottom value set to another value besides 0 if you are showing a keyboard and have the offset set to allow the content at the bottom to show. – Brennan Aug 26 '15 at 15:06
  • 4
    Less verbose way to do it: `self.tableView.contentInset = UIEdgeInsetsMake(MAX(0.0, self.tableView.bounds.size.height - self.tableView.contentSize.height), 0, 0, 0);` . Table view's content size is the size of all the cells. – JoJo Aug 29 '15 at 01:21
5

You can set a header in your table view and make it tall enough to push the first cell down. Then set the contentOffset of your tableView accordingly. I don't think there is a quick built in way to do this though.

Patrick Tescher
  • 3,226
  • 1
  • 15
  • 30
3

I didn't like empty-cell, contentInset or transform based solutions, instead I came up with other solution:

Example

UITableView's layout is private and subject to change if Apple desires, it's better to have full control thus making your code future-proof and more flexible. I switched to UICollectionView and implemented special layout based on UICollectionViewFlowLayout for that (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;
}

It works pretty well for my cases and, obviously, may be optimized by somehow caching content size height. Also, I'm not sure how will that perform without optimizations on big datasets, I didn't test that. I've put together sample project with demo: MDBottomSnappingCells.

Here is Objective-C version:

- (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;
}
MANIAK_dobrii
  • 5,805
  • 3
  • 27
  • 53
  • 1
    I liked your approach. But, I have one question. How can we get dynamic cell height using auto layout in collection view? We can do that in tableview using UITableViewAutomaticDimension. – Nitesh Borad Dec 09 '16 at 06:40
  • I'd suggest against autolayout inside UITableView/UICollectionView cells, but if you still need this you can do that in a number of ways, for collection view I've found that answer: http://stackoverflow.com/q/25895311/1032151 Usually, for UITableView you have a static "sizing cell" that you configure with your data and calculate height, with collection views it works differently, but with the layout I suggest, you may use the same concept. – MANIAK_dobrii Dec 09 '16 at 13:41
2

This can be done by in swift using below funcation

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 }
}
Satish Babariya
  • 2,814
  • 1
  • 13
  • 28
Hiren
  • 270
  • 1
  • 14
2

Here is the Swift 3 version of the @Brennan accepted solution, tested and approved :)

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;
   })

   } 
bkokot
  • 71
  • 6
1

USE THIS. Surely this will help.

- (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];
    }
}
evan
  • 21
  • 1
0
if(indexPath.row!=CategoriesArray.count-1)  
{    
    cell.hidden = YES;
}

return cell;
beryllium
  • 29,214
  • 15
  • 99
  • 123
Poorva
  • 130
  • 6
0

I would resize and re-position the UITableView in its parent view depending on the number of cells. I guess it's the solution that involves minimum workarounds. Also, do you really need to use a UITableView?

0

Add a blank cell in a new section and make it the section at index zero.

-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
    return 2;
}


-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (section==0)
    {
        return 1;
    }
    return [self.yourArray count];
}

Now in

tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath //method add following in beginnig//

if (indexPath.section == 0 )
{
    UITableViewCell * cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
//    cell.backgroundColor = [UIColor clearColor];
    return cell;
}

Now

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0 )
{

    if (self.yourArray.count>0)
    {
        CGFloat totalCellHeight = self.messages.count * yourCellHeight;

        if (totalCellHeight>= self.table.bounds.size.height)
        {
            return 0;
        }
        return self.table.bounds.size.height - totalCellHeight;
    }
    else
        return self.table.bounds.size.height;

}

return yourCellHeight;
}

Now paste this where you are reloading the tableView

[self.table reloadData];
[self.table scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[self.yourArray count]-1 inSection:1] atScrollPosition:UITableViewScrollPositionBottom animated:YES];

It worked for me. Hope this helps.

Abin George
  • 634
  • 6
  • 20
  • In the questions comments, Ohho has specified that, the cell heights are static. So i thought of this one. Thanks Alan for showing it. – Abin George Apr 21 '15 at 13:59
0

Elegant and quick solution without a line of code.

Use container view and put UITableViewController into container( embed segue ).

You can set any height you want to this container.

SevenDays
  • 3,549
  • 10
  • 40
  • 69
0

The best way of doing this I found is to observe the content size of the tableview and adjust the inset if necessary. For example:

static char kvoTableContentSizeContext = 0;

- (void) viewWillAppear:(BOOL)animated
{
    [_tableView addObserver:self forKeyPath:@"contentSize" options:0 context:&kvoTableContentSizeContext];
}

- (void) viewWillDisappear:(BOOL)animated
{
    [_tableView removeObserver:self forKeyPath:@"contentSize"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == &kvoTableContentSizeContext) {

        CGFloat contentHeight = _tableView.contentSize.height;
        CGFloat tableHeight = _tableView.frame.size.height;

        if (contentHeight < tableHeight) {

            UIEdgeInsets insets = _tableView.contentInset;

            insets.top = tableHeight - contentHeight;
            _tableView.contentInset = insets;

        } else {
            _tableView.contentInset = UIEdgeInsetsZero;
        }

    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
jverrijt
  • 666
  • 4
  • 10
0

All answers got some quirks with dynamic rowHeight and/or animations. For me the best working solution was a transformation of the table (flipY):

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

inside cellForRowAt:

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

You may also reverse your data-array and may also flip section-header/footer. Also your footer becomes your new header - but hey, it works.

coyer
  • 2,802
  • 2
  • 21
  • 30
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)
    }

}

I used @bkokot solution with little addition. It does two things

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