I have a UITableView with custom cells. The cells are of a constant height 200. I want to animate the scrolling in of the first cell. This works using scrollToRowAtIndexPath. However, the row appears at the top of the view, whereas I would like it to appear dead center in the visible tableView. I tried setting the tableView's contentInset property to a bunch of different values, but none seem to provide the right offset. Any suggestions?
UITableView actually has a neat method for this:
- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath
atScrollPosition:(UITableViewScrollPosition)scrollPosition
animated:(BOOL)animated
You can use it to scroll the tableView to the point where the cell is at the center of the tableView like this:
[tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:TRUE];
The only way I could find to do this was to use scrollRectToVisible instead of scrollToRowAtIndexPath. For whatever reason (possibly because scrolling on my table was disabled), the positioning was still off even using #Emil's suggestion of combining scrollToRowAtIndexPath with a contentInset.
This works:
tableView.contentInset = UIEdgeInsets(top: cellHeight, left: 0, bottom: cellHeight, right: 0)
tableView.scrollRectToVisible(CGRectMake(0, 0 - tableView.contentSize.height, cellWidth, tableView.contentSize.height), animated: false)
Related
I'm trying to scroll to the bottom of my UITableView (commentsFeed) whenever the user creates a new comment or the user refreshes the UITableView.
The code I use is:
func scrollToBottomOfComments() {
var lastRowNumber = commentsFeed.numberOfRowsInSection(0) - 1
var indexPath = NSIndexPath(forRow: lastRowNumber, inSection: 0)
commentsFeed.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
}
The problem is here in viewDidLoad:
commentsFeed.rowHeight = UITableViewAutomaticDimension
commentsFeed.estimatedRowHeight = 150
This basically states that the comments can have dynamic heights because users could post either really long comments or really short comments.
When I use the estimatedRowHeight, my scrollToBottom doesn't properly scroll to the bottom because it basically assumes my table height is commentsFeed.count * commentsFeed.estimatedRowHeight
This isn't correct though.
When I remove the estimatedRowHeight though, it doesn't seem to work either, and I think the reason is because it doesn't have the row height calculated properly because the rows each have dynamic heights.
How do I mitigate this?
Edit: It should be stated that the scroll doesn't end up at the right position, but the moment I use my finger to scroll anywhere, then the data jumps into place where it should have been via the scroll
Why don't you calculate the real size of row by something similar to below method.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomObject *message = [list.fetchedObjects objectAtIndex:indexPath.row];
//fontChat not available yet
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:message];
NSRange all = NSMakeRange(0, text.length);
[text addAttribute:NSFontAttributeName value:[UIFont fontWithName:DEFAULT_FONT size:21] range:all];
[text addAttribute:NSForegroundColorAttributeName value:RGB(61, 61, 61) range:all];
CGSize theSize = [text boundingRectWithSize:CGSizeMake(200, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
if (theSize.height == 0) {
theSize.height = FIXED_SIZE;
}
return theSize.height;
}
Now for scrolling, I use the following source:
Check it out.
-(void) scrollTolastRow
{
if (self.tableView.contentSize.height > self.tableView.frame.size.height)
{
CGPoint offset = CGPointMake(0, self.tableView.contentSize.height - self.tableView.frame.size.height);
[self.tableView setContentOffset:offset animated:YES];
}
}
There are a couple things that could be affecting your outcome, but it's most likely that your estimated height is not a great estimate. In my experience, anything not particularly close to the true height of the cells will cause havoc on animations. In your case, you mention that the content are comments, or free-form text. I would guess that these cell heights vary wildly, and depending on how your cell is composed, you're probably not going to be able to provide a very accurate estimate, and so you should not provide one. For a large number of cells, this is going to hurt performance, but you probably don't have a choice. Instead, you might want to shift your focus to how you can page in/page out cells into your table to avoid costly calculations, or rearrange your cell to be able to calculate a better estimate. Another suggestion might be to implement estimatedHeightForRowAtIndexPath with an accurate but still less complex algorithm for calculating the height. In any event, a constant value for an estimatedRowHeight will likely never work when you support DynamicType. At the very least, you need to take into account the current DynamicType size.
Other than that, what can you do? Instead of using UITableViewAutomaticDimension, consider implementing heightForRowAtIndexPath and calculating the height of your displayed strings and caching the result (you could use an NSIndexPath -> NSNumber NSCache object). You need to cache the result because without an estimatedHeight, heightForRow is called once for every row when the table is loaded, and then once for every cell as it appears on screen. When using estimatedHeight on iOS 8, estimatedHeight is called once for each cell on launch and heightForRow is called as the cells appear. This is where the estimate is critical, because that is what's used to calculate the contentSize of the UITableView's backing UIScrollView. If the estimated size is wrong, the contentSize is wrong, and so when you ask the tableView to scroll to the last cell, the frame of the last cell is calculated with the bad estimate, which gives you the incorrect contentOffset. Unfortunately, I believe (based on the behavior I see trying to reproduce your question) that when you use UITableViewAutomaticDimension without an estimate, the runtime implicitly estimates an estimate.
EstimatedRowHeight is used by UIKit to estimate whole contentSize (and scrollIndicatorInset), so if you add new row at the end with automatic row dimension, you have to reset estimatedRowHeight to actual average value of whole tableView before you animate scroll.
This is not so good solution because its lot easier to count row height in old style - manually. Or add new cell height value to old table view's content height.
But because you adding new row at the end of table, you can scroll at middle or top position, which ends up with the bottom position, because there is contentInset.bottom = 0. And also the animation will look better.
So:
commentsFeed.scrollToRowAtIndexPath(indexPath, atScrollPosition: .**Middle**, animated: true)
Its look like in .Middle and .Top positions is some condition under the hood in animation which prevent to make a gap between bottom edge and table view's content (+ contentInset.bottom)
P.S. Why not to use the manuall row height calculation?
Because there is autolayout and I believe the "auto" is shortcut for automatic. And also it will save your time and troubles with custom fonts, attributed string, combined cell with more labels and other subviews and so on..
Try this, works for me:
NSMutableArray *visibleCellIndexPaths = [[self.tableView indexPathsForVisibleRows] mutableCopy];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:visibleCellIndexPaths withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
[self.tableView scrollRectToVisible:CGRectMake(0, self.tableView.contentSize.height - self.tableView.bounds.size.height, self.tableView.bounds.size.width, self.tableView.bounds.size.height) animated:YES];
Here is the code I use:
//inserting a row at the bottom first
_numberOfRecords++;
[_tableView beginUpdates];
[_tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:_numberOfRecords-1 inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
[_tableView endUpdates];
//clear text
_inputField.text = #"";
//then scroll to bottom
CGPoint bottomOffset = CGPointMake(0, _tableView.contentSize.height + 44.0 + _tableView.contentInset.top - _tableView.bounds.size.height);
NSLog(#"%f", _tableView.contentSize.height + 44.0 + _tableView.contentInset.top - _tableView.bounds.size.height);
[_tableView setContentOffset:bottomOffset animated:YES];
This would scroll the tableview in a very strange way.
But if I put the scrolling code BEFORE the insertion, it works fine except that it ignores the latest inserted row. That is, it scrolls to the second last row instead of scrolling to the very last row (of course, because it scrolls before inserting a new roll.)
So I believe this code has no problem of the position where it should scroll to.
The problem probably comes from row insertion to tableview.
It violates the animation of scrolling the table view.
I am doing this to make a chatting view.
Each time the user sends or receives a message, I insert a row containing the message to a table view, and scrolls it to the bottom. That's why I use tableView here. I tried to use scrollView with label, it works fine, but tableView seems more popular in a chatting view.
I was thinking to use scrollView or tableView, and I found the built-in message app of Apple is using a tableView, so I adopt tableView. Let me know if a scrollView with Label is better than a tableView.
Anyway, how can I scroll a tableView to the bottom after inserting a new row?
Try using UITableView's scrollToRowAtIndexPath::
[self.tableView scrollToRowAtIndexPath: atScrollPosition: animated:];
This is my own solution:
[_tableView reloadData];
//scroll to bottom
double y = _tableView.contentSize.height - _tableView.bounds.size.height;
CGPoint bottomOffset = CGPointMake(0, y);
NSLog(#"after = %f", y);
if (y > -_tableView.contentInset.top)
[_tableView setContentOffset:bottomOffset animated:YES];
Firstly reloadData after endUpdates. This ensures the tableView contentSize is updated after inserting a new row. Then check if the scrolling distance is greater than the contentInset.top (this is for avoiding the tableview hiding behind the status bar and navigation bar) then to scroll down, otherwise not to scroll because of some weird animation.
Alternatively, you can simply use
[self.tableView scrollToRowAtIndexPath: inSection: atScrollPosition: animated:];
to scroll to the row you want. But this doesn't handle cells with sections and footers very well. For plain tableViewCell, you can just use this to do the magic. Otherwise you may find my trick solution performs better.
Anyway, thanks for all your answers.
I know how to animate the height change of a UITableViewCell using the method seen here: Can you animate a height change on a UITableViewCell when selected?
However, using that method, the UITableView will scroll at the same time, which I don't want it to do.
I have a UITableView with very few cells; it takes up less than the screen height. The bottom cell has a UITextField and, when it starts editing, I manually set the content offset of the UITableView so that the cell with the UITextField is scrolled to the top. Then, based on the input in the UITextField, I may want to increase the size of the UITableViewCell to show extra options, more or less.
The problem is that, when animating this change, it will reposition the UITableView so that my UITextField is no longer at the top.
This is what I'm doing, more or less:
self.customAmountCellSize = height;
[self.tableView beginUpdates];
[self.tableView endUpdates];
I have also tried
self.customAmountCellSize = height;
CGPoint originalOffset = self.tableView.contentOffset;
[self.tableView beginUpdates];
[self.tableView endUpdates];
[self.tableView setContentOffset:originalOffset animated:NO];
I want the row height animation, I do not want the UITableView to scroll as a result.
Any ideas?
It appears the problem you're encountering is that your table view is scrolled past the bottom so when you update its content it will attempt to fix that.
There are two approaches you could take to prevent scrolling:
Set the table view's content inset to be the height of the initial white space:
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, verticalGap, 0);
Add an empty footer view with the same height as the vertical gap:
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, verticalGap)];
In both cases, you will need to calculate the vertical space you are trying to achieve. You then need to restore the contentInset or tableFooterView to its original state when you are done.
I think the table view is scrolling because your text field is becoming the first responder, and not because of the cell height change. Try keeping the cell height the same and just adjusting the offset to be sure.
If I am correct, than here's the solution: UITableView automatically tries to scroll when your keyboard appears. To fix this, set the content offset to your desired offset in a dispatch to the main queue, which will fire at the beginning of the next runloop. Put the following code inside your response to a UIKeyboardWillShowNotification, or in a UITextFieldDelegate shouldBeginEditing method:
// Get the current offset prior to the keyboard animation
CGPoint currentOffset = self.tableView.contentOffset;
UIEdgeInsets currentInsets = self.tableView.contentInset;
__weak SomeTableViewControllerClass *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animationWithDuration:0 animations:{
// set the content offset back to what it was
weakSelf.tableView.contentOffset = currentOffset;
weakSelf.tableView.contentInset = currentInsets;
} completion:nil];
});
Similar fixes are sometimes necessary for the contentInset.bottom of a table view, depending on the frame of your UITableView and other factors.
I have a UITableView in the grouped style, and only one section. However there is some blank space above and below the table view that is shown when the user scrolls too far. How can I remove this blank space?
You can do this by altering the contentInset property that the table view inherits from UIScrollView.
self.tableView.contentInset = UIEdgeInsetsMake(-20, 0, -20, 0);
This will make the top and bottom touch the edge.
Add this code:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 0;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
return 0;
}
Actually this question answered my question.
Reducing the space between sections of the UITableView.
UIView can be inserted at the top and bottom of the table(drag and drop). Set their properties as transparent and height of 1 px. This is to remove the extra padding in front of the cells.
you can also use this code for removing space between first cell of uitableview..
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 0.002f;// set this...
}
Uncheck Extend Edges Under Top bar.
This answer comes quite late, but I hope it helps someone.
The space is there because of the UITableView's tableHeaderView property. When the the tableHeaderView property is nil Apple defaults a view. So the way around this is to create an empty view with a height greater than 0. Setting this overrides the default view thereby removing the unwanted space.
This can be done in a Storyboard by dragging a view to the top of a tableView and then setting the height of the view to a value of 1 or greater.
Or it can be done programmatically with the following code:
Objective-C:
CGRect frame = CGRectZero;
frame.size.height = CGFLOAT_MIN;
[self.tableView setTableHeaderView:[[UIView alloc] initWithFrame:frame]];
Swift:
var frame = CGRect.zero
frame.size.height = .leastNormalMagnitude
tableView.tableHeaderView = UIView(frame: frame)
Comments
As others have noted you can use this same solution for footers.
Sources and Acknowledgements
See the Documentation for more details on the tableHeaderView property.
Thanks to #liushuaikobe for verifying using the least positive normal number works.
My original answer: https://stackoverflow.com/a/22185534/2789144
In my case issue was with the constraints i was applying. I have to change them in order to show 2 rows in my case while bottom of table touching last row.
Use the bounces property of UIScrollView:
[yourTableView setBounces:NO];
This will remove what seems to be an extra padding at the top and bottom of your UITableView.
Actually, it will just disable the tableview's scrollview to scroll past the edge of the content.
I need to add some blank space to the top of my UITableView that does not affect the size of the content area. Shifting the content down or adding a blank cell is NOT what I want to do. Instead I just want an offset.
How?
I'm not sure if I'm following you but I think I'm having the same predicament. In my case I must give some space to the ADBannerView at the top of the screen so what I did was in the viewDidLoad method I added:
[self.tableView setContentInset:UIEdgeInsetsMake(50,0,0,0)];
the values it takes are UIEdgeInsetsMake(top,left,bottom,right).
Alternatively the same with Swift:
self.tableView.contentInset = UIEdgeInsetsMake(50, 0, 0, 0)
Swift 4.2:
self.tableView.contentInset = UIEdgeInsets(top: 50, left: 0, bottom: 0, right: 0)
Swift 5.1
add the following in viewDidLoad
tableView.contentInset.top = 100
Really that's all there is to it.
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset.top = 100
}
You can add an "empty" header view to the table... this would give the initial appearance of the table to have an offset, but once you started scrolling the offset would be gone. NOt sure that's what you want.
If you need a permanent offset and are not already using section headers, then you could create the offset similarly to above by making custom views for the section headers, especially if you just have one section, this could give you the look of a permanent offset.
I can post sample code if it sounds like either of those are what you are looking for.
I combined Jigzat's answer with:
[self.tableView scrollRectToVisible:CGRectMake(0, 0, 320, 1) animated:NO];
in
- (void)viewDidLoad
so the first cell isn't at the top.
Sounds like you want to wrap a 'View' around your UITableView. If you have a UITableViewController in IB the UITableView will automatically be set to the view of UITableViewController. You change view property to a normal UIView and add your UITableView in there and give it a offset.
---Edit---
I just read my post and thought it made little sense :) When you create a UITableViewController you get this (in pseudo code):
UITableViewController.view = UITableView
This means that the actual table will take up the whole space and you cannot even add other views. So you need to change the
UITableViewController.view = UIView
and add your table to that UIView
I combined this answer with this one:
https://stackoverflow.com/a/9450345/1993937
To make the tableView appear at the top of the content inset, so the space at the top isn't cut off by having the tableView scrolled down slightly when the view initially appears. (18 is my top gap)
[self.tableView setContentInset:UIEdgeInsetsMake(18,0,0,0)];
[self.tableView setContentOffset:
CGPointMake(0, -self.songListTable.contentInset.top) animated:YES];