Rodrigo Ruiz
Rodrigo Ruiz

Reputation: 4355

UITableView - Scroll to bottom before viewDidAppear

In case it's not obvious by the title, what I want should be simple, for the tableView to start scrolled all the way to the bottom when the users first sees it (before he sees it, and without animations).

So, I know that this has been answered a few times, but none of those solutions seem to work right now. To give some context, I'm using Swift, autolayout and latest version of iOS as of today.

Constraints

There are some things I need to support:

  1. load it before user sees it (without animations, obviously).
  2. dynamic cell heights, i.e., their height is determined by a UILabel (like a messaging app - using autolayout in storyboard).
  3. My UITableView is inside a UIScrollView (UIScrollView scrolls horizontally and UITableView scrolls vertically).
  4. The UITableView is in a childViewController, i.e., I add it to the main UIViewController (which, for some reason, makes viewWillAppear not get called - viewDidAppear is still called).

I'll make a summary of what I tried:

As for implementations:

  1. tableView.contentOffset = CGPoint(x: 0, y: CGFloat.max

  2. tableView.scrollToRowAtIndexPath(indexPathForLastItem, atScrollPosition: .Bottom, animated: false)


    var contentOffset = tableView.contentOffset
    contentOffset.y = tableView.contentSize.height - tableView.bounds.size.height
    tableView.setContentOffset(contentOffset, animated: false)


    dispatch_async(dispatch_get_main_queue(), {
        // tried option 1., 2. and 3. here
    })

As for places I've tried to call those implementations:

  1. viewDidLoad
  2. viewWillAppear
  3. viewDidLayoutSubviews (only the first time it's called, I use a property to track that)
  4. viewDidAppear (even though this wouldn't give me what I want)
  5. On my model's didSet

Before those I always called tableView.reloadData

What I DON'T want to do:

tableView.transform = CGAffineTransformMakeScale(1, -1)

+

cell.transform = CGAffineTransformMakeScale(1, -1)

(I'm assuming that if you think of suggesting this solution, it's because you know the hack I'm talking about. If you don't, then you won't be suggesting this, so you don't need to understand how it works)

One of the reasons why I don't want this one is because now I can't scroll to the top...

Problems I've noticed:

  1. The tableView's contentSize (as a UIScrollView subclass) changes when you scroll for the first time after it appeared. Yes, I meant contentSize, not contentOffset, and it changes more than once while you scroll. After you've scrolled through the entire tableView once, it doesn't change anymore.
  2. Some people are saying it works for fewer cells (and to be honest, it was working for me at some point), so try it with at least 20 items.
  3. viewWillAppear doesn't get called, but viewDidAppear does.

Any solutions (except for the one I mentioned I don't want) would be very much appreciated. Even hacky ones, as long as they don't break other stuff.

Just as a side note, scrollToRowAtIndexPath... does not scale, i.e., it is too slow if you have, say, 2000 items. So Ideally I'd prefer a solution that scales.

EDITS AFTER ANSWERS:

  1. After @bsmith11's answer I tried:

    private var called = false
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    
        if !called {
            dispatch_async(dispatch_get_main_queue(), {
                self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.bounds.height), animated: false)
            })
            called = true
        }
    }
    

    And it didn't work.

Upvotes: 8

Views: 3272

Answers (7)

hothead
hothead

Reputation: 131

  1. Uncheck "Automatic" tableview's properties ; row Height and Estimate

enter image description here

  1. Add this code

    override func viewWillAppear(_ animated: Bool) {
    
        super.viewWillAppear(animated)
    
        if scrollToBottomOnce == false {
            scrollToBottomOnce = true
            if self.items.count > 0 {
                self.tableView.setContentOffset(CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude), animated: false)
            }
        }
    }
    

Upvotes: 0

iOS Akatsuki
iOS Akatsuki

Reputation: 31

I think that calling [table setContentOffset:CGPointMake(0, CGFLOAT_MAX)] and table.estimatedRowHeight = 50(change your value); in ViewDidLoad is what you need.

Upvotes: 0

imperator
imperator

Reputation: 11

private var called = false
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if !called {
        dispatch_async(dispatch_get_main_queue(), {
            self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.bounds.height), animated: false)
        })
        called = true
    }
}

I tried this code with my UIScrollView. And it works. If I have dispatch_async removed it become not working.

Swift 3 code

private var called = false
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if !called {
        DispatchQueue.main.async(execute: {
            self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.bounds.height), animated: false)})
        called = true
    }
}

Upvotes: 1

bsmith11
bsmith11

Reputation: 296

In viewDidLayoutSubviews(), set the .contentOffset to tableView.bounds.height. You will need to kick this off in an async block to give the tableView time to load it's content.

dispatch_async(dispatch_get_main_queue()) { self.tableView.setContentOffset(tableView.bounds.height, animated: false) }

viewDidLayoutSubviews() can get called multiple times, so you probably want to make sure your code above only gets called once.

Upvotes: 4

Ankur Teotia
Ankur Teotia

Reputation: 227

i had the exact same problem, after trying everything(same as you), this worked, the key is if you're using autolayout , you must write scrollToBottom code in viewDidLayoutSubviews

initialize scrollToBottom to true and then do this

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    // Scroll table view to the last row
    [self scrollToBottom];
}

-(void)scrollToBottom {
    if (shouldScrollToLastRow)
    {
        CGPoint bottomOffset = CGPointMake(0, self.tableView.contentSize.height - self.tableView.bounds.size.height);
        [self.tableView setContentOffset:bottomOffset animated:NO];
    } }

doing this will ensure you're almost at the bottom of you're tableView but might not be at the very bottom as its impossible to know the exact bottom offset when you're at the top of the tableView, so after that we can implement scrollViewDidScroll

-(void)scrollViewDidScroll: (UIScrollView*)scrollView
{
    float scrollViewHeight = scrollView.frame.size.height;
    float scrollContentSizeHeight = scrollView.contentSize.height;
    float scrollOffset = scrollView.contentOffset.y;

    // if you're not at bottom then scroll to bottom
    if (!(scrollOffset + scrollViewHeight == scrollContentSizeHeight))
    {
        [self scrollToBottom];
    } else {
    // bottom reached now stop scrolling
        shouldScrollToLastRow = false;
    }
}

Upvotes: 0

Hitesh Surani
Hitesh Surani

Reputation: 13577

add the following method to custom subclass.

 - (void)tableViewScrollToBottomAnimated:(BOOL)animated {
        NSInteger numberOfRows = [_tableView numberOfRowsInSection:0];
        if (numberOfRows) {
            [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:numberOfRows-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
        }
    }

Calling [self tableViewScrollToBottomAnimated:NO] at the end of viewDidAppear works. One concern is there Unfortunately, it also causes tableView:heightForRowAtIndexPath: to get called three times for every cell.

Upvotes: 0

Hasya
Hasya

Reputation: 9898

Just tried with below code it works fine.

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(true)

      myArray = [["Data": "1234567890", "Height": 44],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "12345678901234567890", "Height": 88],["Data": "End of Table", "Height": 132]]

    let no = myArray.count-1

    let lastIndex = NSIndexPath(forRow: no, inSection: 0)

    table1.scrollToRowAtIndexPath(lastIndex, atScrollPosition: .Bottom, animated: true)

}

Download sample code

Upvotes: 1

Related Questions