If we have 100 records, perhaps we can GET them in one short. But what if, we have 1000 or even 10k records?

Thus, here I shows an example on implement lazy loading on iOS app, as well as pull to refresh.

Dependencies

Create a UIViewController

MyListViewController.h

1
2
3
@interface MyListViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

@end

MyListViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#import <QuartzCore/QuartzCore.h>
#import "MyListViewController.h"
#import "AFNetworking.h"
#import "UIScrollView+SVPullToRefresh.h"
#import "UIScrollView+SVInfiniteScrolling.h"

static int initialPage = 1; // paging start from 1, depends on your api

@interface MyListViewController ()

@property (nonatomic, strong) UITableView *tableView;

// to keep track of what is the next page to load
@property (nonatomic, assign) int currentPage;
// to keep the objects GET from server
@property (nonatomic, strong) NSMutableArray *myList;

@end

@implementation MyListViewController

@synthesize tableView = _tableView;

@synthesize currentPage = _currentPage;
@synthesize myList = _myList;

- (void)viewDidLoad
{
[super viewDidLoad];

// initialize
_myList = [NSMutableArray array];
_currentPage = initialPage;

// init table list
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin| UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleHeight;
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self.view addSubview:self.tableView];

__weak typeof(self) weakSelf = self;
// refresh new data when pull the table list
[self.tableView addPullToRefreshWithActionHandler:^{
weakSelf.currentPage = initialPage; // reset the page
[weakSelf.myList removeAllObjects]; // remove all data
[weakSelf.tableView reloadData]; // before load new content, clear the existing table list
[weakSelf loadFromServer]; // load new data
[weakSelf.tableView.pullToRefreshView stopAnimating]; // clear the animation

// once refresh, allow the infinite scroll again
weakSelf.tableView.showsInfiniteScrolling = YES;
}];

// load more content when scroll to the bottom most
[self.tableView addInfiniteScrollingWithActionHandler:^{
[weakSelf loadFromServer];
}];
}

- (void)loadFromServer
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:[NSString stringWithFormat:@"http://api.example.com/list/%d", _currentPage] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {

// if no more result
if ([[responseObject objectForKey:@"items"] count] == 0) {
self.tableView.showsInfiniteScrolling = NO; // stop the infinite scroll
return;
}

_currentPage++; // increase the page number
int currentRow = [_myList count]; // keep the the index of last row before add new items into the list

// store the items into the existing list
for (id obj in [responseObject valueForKey:@"items"]) {
[_myList addObject:obj];
}
[self reloadTableView:currentRow];

// clear the pull to refresh & infinite scroll, this 2 lines very important
[self.tableView.pullToRefreshView stopAnimating];
[self.tableView.infiniteScrollingView stopAnimating];

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
self.tableView.showsInfiniteScrolling = NO;
NSLog(@"error %@", error);
}];
}

- (void)reloadTableView:(int)startingRow;
{
// the last row after added new items
int endingRow = [_myList count];

NSMutableArray *indexPaths = [NSMutableArray array];
for (; startingRow < endingRow; startingRow++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:startingRow inSection:0]];
}

[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
}


#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
id item = [_myList objectAtIndex:indexPath.row];
NSLog(@"Selected item %@", item);
}

#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [_myList count];
}

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

// minus 1 because the first row is the search bar
id item = [_myList objectAtIndex:indexPath.row];

cell.textLabel.text = [item valueForKey:@"name"];

return cell;
}

@end

EDIT:

Remember to set the autoresizingMask, e.g.

1
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin| UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleHeight;

Otherwise the contentSize of scrollView will have problem when calling showsInfiniteScrolling = NO;

See the images below:

Not working example

content size problem

Working example

content size solved

Done :)