43

How do I do an infinite scrolling in a UITableView? I know how to do it using a UIScrollView, in which apple has demonstrated in one of the WWDC's video. I tried doing the following in tableView:cellForRowAtIndexPath::

if (indexPath.row == [self.newsFeedData_ count] - 1)
{
    [self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
    [self.tableView reloadData];
}

but this fails. Any other idea?

gardenofwine
  • 1,314
  • 2
  • 15
  • 24
adit
  • 28,968
  • 65
  • 214
  • 354
  • If you NSLog `self.newsFeedData_` before and after you call `[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];` is the output the same? (Maybe start with just outputting `[self.newsFeedData_ count]` and see if the number of records in the array has increased? – runmad May 01 '12 at 21:30
  • Here is a demo of infinite UITableView in Swift: https://github.com/i-schuetz/tableview_infinite – Ixx Jun 27 '14 at 19:41
  • I've added [an answer here](http://stackoverflow.com/a/42902171/882630) that uses the UITableViewDelegate table​View(_:​will​Display:​for​Row​At:​) instance method very simply. – lukkea Mar 20 '17 at 11:26
  • Take a look:- https://stackoverflow.com/a/63201282/10563627 – Paresh Mangukiya Aug 01 '20 at 02:54
  • https://github.com/canopas/MarqueeScroll – SPatel Jan 19 '21 at 04:43

9 Answers9

56

If you need to know when you hit the bottom of the UITableView, become it's delegate (because it is a subclass of UIScrollView), and use the -scrollViewDidScroll: delegate method to compare the table's content height and it's actual scroll position.

EDIT (something like this):

- (void)scrollViewDidScroll:(UIScrollView *)scrollView_ 
{   
    CGFloat actualPosition = scrollView_.contentOffset.y;
    CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
    if (actualPosition >= contentHeight) {
        [self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
        [self.tableView reloadData];
     }
}
CodaFi
  • 42,165
  • 8
  • 102
  • 150
  • hmm... could it be that your array is somehow empty? – CodaFi May 01 '12 at 21:34
  • You must have one uniqueNewsFeedData_ and add objects from that array because if you add objects from same array than array size increases like x , 2x , 4x , 8x. – iDev Jul 31 '13 at 09:45
  • 1
    If anyone is curious, the `someArbitraryNumber` shown turned out to be the height of the screen for me – Hari Ganesan Jul 02 '14 at 17:25
  • this works perfectly for scrolling to the bottom, but what about the top? – jsetting32 Nov 11 '14 at 10:17
  • 9
    I used as `someArbitraryNumber` the `tableView.frame.size.height` – Daniel Gomez Rico Dec 29 '14 at 20:44
  • This works, however you have to manage process (i.e., fetching, complete) of adding new items. This delegate method is called hundreds of times because it is fired while scrolling. – Sententia Jan 24 '15 at 08:42
  • @adit Sorry for too late a reply but did this answer work while your original code didn't? Was there any mistake in your side (e.g. empty list)? – Blaszard Sep 21 '15 at 16:46
  • NEW: on iPad Pro, the screen is very tall. To get consistent behavior across devices, I use the bottom position instead: `CGFloat actualPosition = scrollView_.contentOffset.y + scrollView.frame.size.height;` – AndyDeveloper Apr 29 '16 at 20:36
  • If your new data takes a while to fetch, you may want to make `someArbitraryNumber` considerably larger than the screen height. – BallpointBen Jul 12 '17 at 15:46
18

You can support infinite scroll with pull to refresh at the top and/or scroll continuously at the bottom with a spinner wheel using:

https://github.com/samvermette/SVPullToRefresh

SVPullToRefresh handles the logic when UITableView reaches the bottom. A spinner is shown automatically and a callback block is fired. You add in your business logic to the callback block.

Here's an example:

#import "UIScrollView+SVInfiniteScrolling.h"

// ...

[tableView addInfiniteScrollingWithActionHandler:^{
    // append data to data source, insert new cells at the end of table view
    // call [tableView.infiniteScrollingView stopAnimating] when done
}];

This project can be added to your project using CocoaPods or directly compiled into your project.

Richard H Fung
  • 1,026
  • 12
  • 12
17

Here's a very quick and complete demo of an infinite scrolling UITableView I put together...

@interface InfiniteScrollViewController ()

@property (nonatomic) NSMutableArray *tableViewData;
@property (nonatomic) BOOL loadingMoreTableViewData;

@end

@implementation InfiniteScrollViewController

- (void)viewDidLoad {
    self.tableViewData = [[NSMutableArray alloc] init];
    [self addSomeMoreEntriesToTableView];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.tableViewData.count + 1;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    if (indexPath.row < self.tableViewData.count) {
        cell.textLabel.text = [self.tableViewData objectAtIndex:indexPath.row];
    } else {
        cell.textLabel.text = @"Loading more data...";

        // User has scrolled to the bottom of the list of available data so simulate loading some more if we aren't already
        if (!self.loadingMoreTableViewData) {
            self.loadingMoreTableViewData = YES;
            [self performSelector:@selector(addSomeMoreEntriesToTableView) withObject:nil afterDelay:5.0f];
        }
    }

    return cell;
}

- (void)addSomeMoreEntriesToTableView {
    int loopTill = self.tableViewData.count + 20;
    while (self.tableViewData.count < loopTill) {
        [self.tableViewData addObject:[NSString stringWithFormat:@"%i", self.tableViewData.count]];
    };
    self.loadingMoreTableViewData = NO;
    [self.tableView reloadData];
}

@end
Oliver Pearmain
  • 17,376
  • 12
  • 77
  • 83
  • 1
    [self.tableView reloadData] can cause page flickering..i know i can use [self.tableView beginUpdates] and [self.tableView endUpdates] but not sure how to use it correctly.. – trillions Mar 04 '13 at 07:17
  • 1
    Yep, within my 'addSomeMOreEntriesToTableView' method instead of calling reloadData you would create an array of NSIndexPath's, then call beginUpdates, followed by insertrowAtIndexPaths:withRowAnimations: (passing the array of indexPaths) and finally endUpdates. – Oliver Pearmain Mar 04 '13 at 08:41
  • thanks! I will try it out in the morning, too tired now~will update :) – trillions Mar 04 '13 at 08:51
  • You might be better off using the delegate method `- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath`. This way you could avoid having to user `performSelector` – Sam Clewlow Jan 20 '16 at 10:05
12

'UITableView' is same as 'UIScrollView' in 'scrollViewDidScroll' method.

So, its easy to emulate infinite scrolling.

  1. double the array so that head and tail are joined together to emulate circular table

  2. use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.

:

/* To emulate infinite scrolling...

The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualising joined table in eight parts)

When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.

Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)

In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
*/


-(void)scrollViewDidScroll:(UIScrollView *)scrollView_ 
{  

    CGFloat currentOffsetX = scrollView_.contentOffset.x;
    CGFloat currentOffSetY = scrollView_.contentOffset.y;
    CGFloat contentHeight = scrollView_.contentSize.height;

    if (currentOffSetY < (contentHeight / 8.0)) {
    scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
    }
   if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
       scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
    }

}

P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8

If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit the method when data count is say for example less than 9.

MIWMIB
  • 1,235
  • 1
  • 12
  • 24
  • Hi, can you please let me know the calculation for "8.0" and "((contentHeight * 6)/ 8.0)"? Is it related to "no of rows" in tableview? Please let me know. – Nishant B May 20 '13 at 09:59
  • No its not. It means 6/8th of full length of the table (not just visible area). So when user reaches towards the end (6/8th) of the table, they are taken to the first part fo the data (as we have doubled the data) where the data is same. Similarly, when scroll upwards to 1/8th of the table, they are taken to second part of the table where the data is same. – MIWMIB May 23 '13 at 03:37
5

For me worked better scrollViewDidEndDragging: than scrollViewDidScroll:.

The second approach will send you each position during scroll and cause, if you are fetching remote resources you will hit your endpoint several times, which is not good.

Complete example based on @codafi solution with comments from @danielgomezrico about how to calculate contentHeight:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
                  willDecelerate:(BOOL)decelerate {
    CGFloat actualPosition = scrollView.contentOffset.y;
    CGFloat contentHeight = scrollView.contentSize.height - (self.tableView.frame.size.height);
    if (actualPosition >= contentHeight) {
        // fetch resources
        [self.tableView reloadData];
    }
}
zevarito
  • 364
  • 4
  • 9
3

Generally I override scrollViewDidEndDecelerating and inside it I put my code to request more data.
Example:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{

    float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;

    if (endScrolling >= scrollView.contentSize.height){
        //put here your code

    }
}

Recently I uploaded on GitHub a subclass of UITableView, that implements the infinite scroll.
You can download it here:
https://github.com/alchimya/iOS-LazyTableView

1

rather than overriding we can do this optimally in layoutSubviews. Here's how I got it implemented. You can get to know more about the implementation here

- (void)layoutSubviews{
[super layoutSubviews];

if(self.delegateForViews){

    CGPoint contentOffset = self.contentOffset;

    if([self.delegateForViews noOfViews]>numOfReusableViews){
        NSUInteger centerIndex=visibleViews.count/2;
        NSUInteger noOfViews=[self.delegateForViews noOfViews];
        UIView *centerView=[visibleViews objectAtIndex:centerIndex];

        CGPoint centerViewOrigin=centerView.frame.origin;
        CGSize centerViewSize=centerView.frame.size;
        CGFloat offsetDifference=contentOffset.x-centerViewOrigin.x;
        CGFloat offsetDifferenceAbs=fabs(contentOffset.x-centerViewOrigin.x);

        if(offsetDifferenceAbs>=centerViewSize.width){

            if(offsetDifference<0){
                currentPosition--;
            }else{
                currentPosition++;
            }

            self.contentOffset=centerViewOrigin;

            currentPosition=[self getPosition:currentPosition noOfViews:noOfViews];

            [self.delegateForViews clearView:centerView];
            [self.delegateForViews setupView:centerView forPosition:currentPosition];

            for (int i=centerIndex-1; i>=0; i--) {
                UIView* prevView=[visibleViews objectAtIndex:i];
                [self.delegateForViews clearView:prevView];
                [self.delegateForViews setupView:prevView forPosition:
                        [self getPosition:currentPosition-1 noOfViews:noOfViews]];
            }

            for (int i=centerIndex+1; i<visibleViews.count; i++) {
                UIView* nextView=[visibleViews objectAtIndex:i];
                [self.delegateForViews clearView:nextView];
                [self.delegateForViews setupView:nextView forPosition:
                        [self getPosition:currentPosition+1 noOfViews:noOfViews]];
            }

        }
    }

}

}
Shardul
  • 26,423
  • 5
  • 33
  • 35
0

One of the simple and that offered me everything i need is this class:

https://github.com/jakemarsh/JMStatefulTableViewController

You just need to subclass JMStatefulTableViewController and the it has 3 methods that you need to overwrite:

  • one that is called on init, to get the initial data
    • statefulTableViewControllerWillBeginInitialLoading
  • one when the user pull to refresh
    • statefulTableViewControllerWillBeginLoadingFromPullToRefresh
  • one when is called for the infinite scroll (next page)
    • statefulTableViewControllerWillBeginLoadingNextPage

This can be used from Cocoapods too.

Cornel Damian
  • 712
  • 8
  • 11
0

scrollviewDidScroll will call when you move through the rows in tableview

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    //check for the visible rows
    let indexpath = self.tableView.indexPathsForVisibleRows?.last
    //check if the visible row last is equal to the total number of counts
    if(indexpath?.last == self.listCount){
      //code for adding data to the tableview and reload the table view.
    }
}

look in the link for more details about indexPathForVisibleRows https://developer.apple.com/documentation/uikit/uitableview/1614885-indexpathsforvisiblerows