40
votes

I have a UITableView that needs to introduce new content from the bottom. This is how a table view behaves when the view is full and you add new rows with animation.

I was starting it by altering the contentInset as rows are introduced but then when they change things go off, and the entry animation is all wrong... My problem with this approach is compounded by the fact that users can delete rows, and the row contents update, causing them to resize (each row has it's own height which changes).

Any recommendations on how to get a UITableView rows to always appear at the bottom of the UITableView's view space?

11
I don't see how always ensuring rows are added at the bottom has anything to do with custom cell height or the user's ability to delete rows. What am I missing? - Mark Granoff
I mean visually from the bottom of the View. Example: Fill a table with stuff so you can scroll in it. Add a new line, the line appears off the bottom, you can now scroll to the bottom of the view so the line enters visibility by sliding up from the bottom. Contrast this to the first line you add to the view (which is what I need to adapt), the first line appears at the top of View... I need the first and 50th lines to enter in the same location visually (sliding up from the bottom of the UITableView's area). - ima747
I've solved this, can't post and answer to myself yet, so here's my solution post: Step 1, apply a transform to the table view rotating it 180deg tableView.transform = CGAffineTransformMakeRotation(-3.14); Then rotate your raw cell 180deg in tableView:cellForRowAtIndexPath: The hard part is remembering that left is right and right is left only at the table level, so if you use animations to add or remove you have to flip left/right. You also have to remember to reverse your data source, new entires go at the TOP of your data source (in my case a mutable array) instead of the bottom) - ima747
Wow. I had to read that 3 times to understand your solution. Very inventive, to say the least! I wonder if that is really "easier", given that you have to remember that everything is backwards. :-) Very interesting though! +1 for you! - Mark Granoff
Yea, it's a total head f*** but in practice once you set it up all the animations work the same (inside cells anyway) sizes are the same etc, just have to add in reverse order and tread the table itself left/right backwards... it's a lot easier than trying to manage a bumper space as you add/remove/resize etc. especially since it really only affects things for the first 5 lines or so in most cases... - ima747

11 Answers

74
votes

I've got a solution that works for me perfectly, but it causes a bunch of double thinking so it's not as simple in theory as it is in practice... kinda...

Step 1, apply a transform to the table view rotating it 180deg

tableView.transform = CGAffineTransformMakeRotation(-M_PI);

Step 2, rotate your raw cell 180deg in tableView:cellForRowAtIndexPath:

cell.transform = CGAffineTransformMakeRotation(M_PI);

Step 3, reverse your datasource. If you're using an NSMutableArray insert new objects at location 0 instead of using AddObject...

Now, the hard part is remembering that left is right and right is left only at the table level, so if you use

[tableView insertRowsAtIndexPaths:targetPath withRowAnimation:UITableViewRowAnimationLeft]

it now has to be

[tableView insertRowsAtIndexPaths:targetPath withRowAnimation:UITableViewRowAnimationRight]

and same for deletes, etc.

Depending on what your data store is you may have to handle that in reverse order as well...

Note: rotate the cells OPPOSITE the table, otherwise floating point innacuracy might cause the transform to get off perfect and you'll get crawlies on some graphics from time to time as you scroll... minor but annoying.

44
votes

The accepted method introduces issues for my app - the scroll bar is on wrong side, and it mangles cell separators for UITableViewStyleGrouped

To fix this use the following

tableView.transform = CGAffineTransformMakeScale (1,-1);

and

cell.contentView.transform = CGAffineTransformMakeScale (1,-1);
// if you have an accessory view
cell.accessoryView.transform = CGAffineTransformMakeScale (1,-1); 
17
votes

Similar approach to ima747, but rotating 180 degrees also makes the scrolling indicator go to the opposite side. Instead I flipped the table view and its cells vertically.

self.tableView.transform = CGAffineTransformMakeScale(1, -1); //in viewDidLoad

cell.transform = CGAffineTransformMakeScale(1, -1);//in tableView:cellForRowAtIndexPath:
2
votes

Create a table header that is the height of the screen (in whatever orientation you are in) LESS the height of the of rows you have that you want visible. If there are no rows, then the header is the full height of the table view. As rows are added, simultaneously reduce the height of the table header by the height of the new row. This means changing the height of the frame of the view you provide for the table header. The point is to fill the space above the table rows to give the appearance that the rows are entering from the bottom. Using a table header (or section header) pushes the table data down. You can put whatever you like in the header view, even have it blank and transparent if you like.

This should have the effect you are looking for, I think.

Look at the attribute tableHeaderView. You simply set this to the view you want displayed in the table header. Then you can manipulate it as needed as you add rows. I can't recall just how forceful you then need to be to get the view to actually update in the UI. Might be as simple as calling setNeedsDisplay, if anything.

Alternatively, look at the methods tableView:viewForHeaderInSection: and tableView:heightForHeaderInSection:. Similar to using a table header view, you would want to have an instance variable that you setup once but that you can access from these methods to return either the view itself or its height, respectively. When you need to change the view for the (first) section, you can use reloadSections:withAnimation: to force an update to the view on screen after you have changed the views height (or content).

Any of that make sense? I hope so. :-)

2
votes

Swift 3.01 - Other solution can be, rotate and flip the table view. Works very well for me and not mess with the animation and is less work for the reload data on the table view.

self.tableView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(Double.pi)))
self.tableView.transform = CGAffineTransform.init(translationX: -view.frame.width, y: view.frame.height)
0
votes

I just wanted to add something to all of these answers regarding the use of this technique with UICollectionView... Sometimes when invalidating the layout, my cells would get transformed back the wrong way, I found that in the UICollectionViewCell and UICollectionReusableView subclasses I had to do this:

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    [super applyLayoutAttributes:layoutAttributes];
    [self setTransform:CGAffineTransformMakeScale(1, -1)];
}
0
votes
dataSourceArray = dataSourceArray.reversed()

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

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


func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    if let text = textField.text {
        dataSourceArray.insert(text, at: 0)
        self.tableView.reloadData()
        textField.text = ""
    }
    textField.resignFirstResponder()
    return true
}
0
votes

An easier way is to add the following lines at the bottom of cellForRowAtIndexPath

if(indexPath.section == self.noOfSections - 1)
    [self scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:([self numberOfRowsInSection:self.noOfSections -1] - 1) inSection:self.noOfSections -1] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
0
votes

Late to the party but, inspired by @Sameh Youssef's idea, a function to scroll to the last cell in the tableview, assuming you only have one section. If not, just return the number of sections instead of hardcoding the 0.

The microsecond delay was arbitrarily chosen.

 func scrollToLast() {
        DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(5)) {
            let lastIndex = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0)
            if lastIndex.row != -1 {
                self.tableView.scrollToRow(at: lastIndex, at: .bottom, animated: false)
            }
        }
}
0
votes

I would recommend to use the approach described in this blog post. Have checked it on iOS 12 and 13. Works perfectly.

-2
votes

Well, if you load your tableview with an NSMutableArray i would suggest you to sort out the array in the inverse order. So the table view will be filled up like you want.