40

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?

ima747
  • 4,617
  • 3
  • 31
  • 45
  • 1
    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 Apr 15 '11 at 16:49
  • 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 Apr 16 '11 at 12:25
  • 2
    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 Apr 16 '11 at 13:57
  • 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 Apr 16 '11 at 14:35
  • 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 Apr 16 '11 at 18:59
  • I wrote another possible solution here: http://stackoverflow.com/a/23908328/1771537 – Florian L. May 28 '14 at 11:17

11 Answers11

74

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.

Crashalot
  • 31,452
  • 56
  • 235
  • 393
ima747
  • 4,617
  • 3
  • 31
  • 45
  • `3.14` should be written as `M_PI` to be exact – user102008 Dec 16 '11 at 00:24
  • 3
    alternately, instead of rotating 180 degrees, you can also flip the y axis (again, for both the table and the cell): `CGAffineTransformMakeScale(0, 1)` – user102008 Dec 16 '11 at 00:25
  • Excellent recommendations, thanks! I personally like the 180/-180 method just because it's easier to keep my mind straight vs 180/horiz-flip (which might create additional render fuzzing, I haven't tried it but going 180 and then another 180 made it fuzzy... just a thought) – ima747 Dec 16 '11 at 04:36
  • 1
    Thanks for the solution. I've used it for a very simple grouped tableview example on GitHub if anyone wants to use/modify it: https://github.com/StephenAsherson/FlippedTableView – Stephen Asherson Dec 29 '12 at 12:24
  • An easier solution would be to update the tableview layout each time a cell is added/removed. Its frame would be shrinked and set at the bottom only when its cells do not fill the whole view. This works perfectly for me. To avoid the cell being cut in the middle when it is scrolled, you can either disable the scroll in this case or disable the clipping (you might need another containing view with the full size to get the clipping at the bottom). – Phil Apr 29 '13 at 17:31
  • Phil: Would love to see an example of that. It sounds far more complicated to me, especially as laying out the exact tableview position and size becomes quite tricky since it adjusts, which also would be prone to animation issues (of both cells and all views) I suspect... But maybe I'm missing something. – ima747 Apr 29 '13 at 19:50
  • Great solution, this works well for me. But how to handle the reordering? The gesture while editing seems to be flipped too: I have to drag up to move a cell down. Any ideas? – Christoph Apr 29 '13 at 21:12
  • Suspect you should look at a different approach, like the one suggested by Phil. The whole table is rotated (or flipped depending on how you chose to go at it), due to the nature of the interactions with the gestures and how they're affected by transforms doing all the overrides needed to fix them probably isn't worth it. If you're targeting iOS 6 an later only you could use a collection view... collections solve pretty much any layout problem like this but since they only work on iOS 6 or later legacy support goes out the window. – ima747 Apr 30 '13 at 12:39
  • This will be a bit late comment but does anyone know if it's possible to place the scroll indicators to their original location (right)? It now appears at left because of the transform, and I can't think of a way to put it back right. – Can Poyrazoğlu Feb 23 '14 at 21:57
  • Instead of rotating try flipping as suggested above by user102008. CGAffineTransformMakeScale(0, 1). That should keep the scroll bar on the correct side, however it will still be upside down, which maybe you want but maybe you don't. You could dig into the scroll view higherarchy and play around with the scroll view itself as well but modifying view structures is at a minimum not recommended... – ima747 Feb 24 '14 at 12:58
  • 6
    CGAffineTransformMakeScale(0, 1) would be zero width and same y coords. To flip them use CGAffineTransformMakeScale(1, -1)! – Nick H247 Jul 03 '14 at 14:45
  • Very good explanation thanks, i got my solution by it. Thanks again @ima747 – Ranjit Bisht Apr 29 '16 at 11:31
43

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); 
Nick H247
  • 7,976
  • 3
  • 44
  • 50
17

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:
Jeffrey Sun
  • 6,399
  • 1
  • 21
  • 17
2

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)
Maximo Lucosi
  • 328
  • 4
  • 8
2

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. :-)

Mark Granoff
  • 16,732
  • 2
  • 55
  • 60
  • This was the approach I took at first, but had problems with for a number of reasons: First the rows are flexible in size, AND can change after being added so managing the "bumper" space is quite annoying. Secondly, adjusting the header messes with the scrolling and other animations so things don't enter smoothly. And lastly, when you delete a row you then have to see if you have to add space back to the bumper... – ima747 Apr 16 '11 at 13:48
  • This approach I would highly recommend if you're not doing fancy stuff like animation etc. and your list ONLY grows with no deletes as it's quite easy to manage in that situation, just make the bumper smaller as you add things. – ima747 Apr 16 '11 at 13:49
  • One thing to add you have to manage the header to not get less than 0 in height... – ima747 Apr 16 '11 at 14:05
0

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)];
}
lramirez135
  • 2,622
  • 3
  • 14
  • 17
0
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

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];
AS Mackay
  • 2,463
  • 9
  • 15
  • 23
0

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)
            }
        }
}
Misha Stone
  • 531
  • 5
  • 18
0

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

-2

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.

bobby grenier
  • 2,010
  • 2
  • 14
  • 11
  • That would cause new things to enter at the top, and doesn't address the positioning which is the only problem... – ima747 Apr 16 '11 at 12:22