1

I'm working on a messaging component of an iOS application. Each message is a row on my UITableView _messageTable I tried the following code in viewDidLoad, viewWillAppear and in viewDidAppear (messages are coming in from a background thread):

dispatch_async(dispatch_get_main_queue(), ^{
    [_messageTable reloadData];
});
dispatch_async(dispatch_get_main_queue(), ^{
    [self scrollToBottomOfTable];
});

-(void)scrollToBottomOfTable {
    NSInteger rowCount = [_messageTable numberOfRowsInSection:0];
    if (rowCount > 0) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow: rowCount-1 inSection: 0];
        [_messageTable scrollToRowAtIndexPath: indexPath atScrollPosition: UITableViewScrollPositionBottom animated: YES];
    }
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _privateMessages.count;
}

This code works until there are about 20 or so rows. But when there are more messages, then it doesn't quite scroll all the way to the bottom. Is there some sort of limit? I'm ok with a view load delay if it means scrolling all the way to the bottom. Eventually I will implement some sort of message loading cap, but I would like to first understand and solve this issue.

EDIT

This is a table view within a view controller. I'm saving sent and received Message objects in a User object's array property (the person on the other end of the message conversation. app.hero is the user that is logged in, inheriting form the User object). The thing that doesn't make sense is that the code works perfectly for up to around 20 messages. Beyond that, it doesn't scroll all the way, but I can still manually scroll to the bottom as expected. The rest of the cell config code is :

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
tableView.estimatedRowHeight = 20.0;
tableView.rowHeight = UITableViewAutomaticDimension;
Message *message = [_privateMessages objectAtIndex:indexPath.row];
static NSString *cellId = @"messageCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
    cell.textLabel.numberOfLines=0;
}
return [self configureTableCell:cell forMessage:message];
}

- (UITableViewCell *)configureTableCell:(UITableViewCell *)cell forMessage:(Message *)message {
AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
if (message.sender == app.hero) {
    cell.backgroundColor = [UIColor whiteColor];
    cell.textLabel.textAlignment = NSTextAlignmentRight;
} else {
    cell.backgroundColor = [UIColor colorWithRed:0 green:1 blue:0.2 alpha:0.3];
    cell.textLabel.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
    cell.textLabel.textAlignment = NSTextAlignmentLeft;
}
cell.textLabel.text = message.content;
return cell;
}
Sam S
  • 188
  • 1
  • 9
  • I don't see anything wrong with your current piece. Could you please include more code, specially on how you're updating `_privateMessages` ? It could be a multithreading problem. – Armin Jan 26 '15 at 20:33
  • Is this a table view controller or a view controller with a table view? If the latter, make sure the table view isn't off the bottom of the screen. – rmaddy Jan 26 '15 at 20:36
  • 1
    Are you dynamically setting the heights of cells? – Ian MacDonald Jan 26 '15 at 20:38
  • So, what happens when you attempt to manually scroll down to the hidden row? – Hot Licks Jan 26 '15 at 20:42
  • All, thank for the notes, I've edited the question with more information. – Sam S Jan 26 '15 at 20:59
  • @HotLicks - It seems to stop after about 20 rows, and then goes a little further (not even an entire row height) for each additional row added. Tough to quantify precisely from what I'm seeing. – Sam S Jan 26 '15 at 21:14

2 Answers2

1

I've determined that this issue has to do with cell height. By increasing tableView.estimatedRowHeight to 44.0, the table view scrolls flawlessly to the bottom. However, there remains an issue with when messages wrap beyond 1 line in a given cell. For each message longer than 1 line, the table scroll comes up a few more pixels short. Removing tableView.estimatedRowHeight altogether seems to result in similar behavior as setting it to say, 44.0. I want to say my original question is answered, but I'm still not sure how to make it scroll perfectly given the likelihood of multi-line cells.

EDIT - SOLUTION

The problem of incorrect scrolling is in fact solved by removing UITableViewAutomaticDimension and manually calculating the height in heightForRowAtIndexPath.

Sam S
  • 188
  • 1
  • 9
  • I'm recalling I ran into this once, and it required some careful checking to assure that row heights agreed between the cell itself and the interfaces that ask for it. – Hot Licks Jan 26 '15 at 22:01
  • (What you might want to do is store the cell height in your dataSource, so you can return an accurate value in `heightForRow...` all the time.) – Hot Licks Jan 26 '15 at 22:14
  • (And be sure to implement `heightForRow`. IIRC, you need that if your cells aren't all a standard height.) – Hot Licks Jan 27 '15 at 01:47
  • @HotLicks - let me see if I'm understanding your proposal. Calculate row height manually using `heightForRowAtIndexPath` instead of `UITableViewAutomaticDimension`. Then store each height, and use something like `[_messageTable setContentOffset:bottomOffset animated:YES]` where `bottomOffset` is the calculated by summing the values of all row heights stored? – Sam S Jan 27 '15 at 02:27
  • Seems to be the same problem... http://stackoverflow.com/questions/25686490/ios-8-auto-cell-height-cant-scroll-to-last-row – Sam S Jan 27 '15 at 02:37
  • 1
    Calculate the row height, based on how you will be creating the cell, and return that in the `heightForRow...` (not `estimatedRowHeight`) delegate method. This assures that the table view has accurate row height numbers, and then `scrollToRow...` should work. (Incidentally, you should not be setting tableView.estimatedRowHeight and tableView.rowHeight on every call to `cellForRow`. You would only set those once (if a all) during setup.) – Hot Licks Jan 27 '15 at 03:41
  • I'll look into this solution in the future, as I'm hesitant to attempt manual cell height sizing (it's been a struggle in the past). In the meantime, I've found a solution that works with `UITableViewAutomaticDimension`. – Sam S Jan 27 '15 at 04:40
0

Here is a solution that works well with auto cell height sizing. It's quite a workaround, but this appears to be an Apple bug.

Set _manualScrolling=NO in viewDidLoad.

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (!decelerate) {
        _manualScrolling = NO;
    } else {
        _manualScrolling = YES;
    }
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    _manualScrolling = YES;
}

-(void)reloadTable {
    dispatch_async(dispatch_get_main_queue(), ^{
        [_messageTable reloadData];
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        [self scrollToBottomOfTable];
    });
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (!_manualScrolling && ![self scrolledToBottom]) {
        [self scrollToBottomOfTable];
    }
}

-(BOOL)scrolledToBottom {
    NSInteger rowCount = [_messageTable numberOfRowsInSection:0];
    NSArray *visibleIndices = [_messageTable indexPathsForVisibleRows];
    NSIndexPath *lastVisibleIndex = [visibleIndices lastObject];
    NSIndexPath *lastIndex = [NSIndexPath indexPathForRow: rowCount-1 inSection: 0];
    return lastVisibleIndex.row == lastIndex.row;
}

-(void)scrollToBottomOfTable {
    NSInteger rowCount = [_messageTable numberOfRowsInSection:0];
    if (!rowCount > 0) return;
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow: rowCount-1 inSection: 0];
        [_messageTable scrollToRowAtIndexPath: indexPath atScrollPosition: UITableViewScrollPositionBottom animated: YES];
}
Sam S
  • 188
  • 1
  • 9