iOS UITableView : Load data while scrolling


I did a small POC on how to load data automatically while scrolling the table view. Data is loading over network using a rest API. So I have choose a 500px API to load set of photos. They are providing API to load featured photos and that has lot of data. So they have providing data as pages. (Which is exactly what I wanted.)

We should have a array to keep loaded data in the memory. I have named this array as photos. This will be the data source for the UITableView. We are dynamically updating this array after each successful data retrieval. So it should be mutable.

@property (nonatomic, strong) NSMutableArray *photos;

Keep private properties to keep track of current page and total number of pages. These two variables will be use to decide whether application should make a request to load more data from the server. 500px API is returning totalNumber of items in their API. For the approach I am using currentPage variable and totalPages will be enough. But for more safety I will add another variable to keep track of totalItems as well.

@property (nonatomic, assign) NSInteger currentPage;
@property (nonatomic, assign) NSInteger totalPages;
@property (nonatomic, assign) NSInteger totalItems;

To load data, I wrote a separate method, which will accept page parameter. After a successful response, photos array will be updated with new elements. In addition to that currentPage, totalPages and totalItems properties will also updated / reset according to the server response. Usually updating the value once is sufficient. But I am expecting two advantages with approach.

  • If the system updating frequently, new data will be available at any time. So problem of not having updated data will avoid with this.
  • Sometimes because of programming errors values of above properties will take wrong values. So we can be confident about having correct page numbers.

Finally we are reload data in the table view.

- (void)loadPhotos:(NSInteger)page {
    
    NSString *apiURL = [NSString stringWithFormat:@"https://api.500px.com/v1/photos?feature=editors&page=%ld&consumer_key=%@",(long)page,kConsumerKey];
    
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:[NSURL URLWithString:apiURL]
            completionHandler:^(NSData *data,
                                NSURLResponse *response,
                                NSError *error) {
                
                if (!error) {
                    
                    NSError *jsonError = nil;
                    NSMutableDictionary *jsonObject = (NSMutableDictionary *)[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError];
                    
                    [self.photos addObjectsFromArray:[jsonObject objectForKey:@"photos"]];
                    
                    self.currentPage = [[jsonObject objectForKey:@"current_page"] integerValue];
                    self.totalPages  = [[jsonObject objectForKey:@"total_pages"] integerValue];
                    self.totalItems  = [[jsonObject objectForKey:@"total_items"] integerValue];
                    
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [self.tableView reloadData];
                    });
                }
            }] resume];
}

Now the things about the UITableView. When deciding number of rows, we should first check whether there are more data to load. If the current page is equal to totalNumber pages that means we are done. We loaded all the available data from the server. For more safety we can check for totalItems with the size of the photos array. If current page is not equal to total number of pages, We increment the numer of rows by 1 to show a additional cell with loading activity indicator.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.currentPage == self.totalPages
        || self.totalItems == self.photos.count) {
        return self.photos.count;
    }
    return self.photos.count + 1;
}

In tableView:willDisplayCell:forRowAtIndexPath: method, we checking whether the cell which going to display is last cell. That’s the cell with the loading activity indicator. If so we are making a server call to load data for next page.

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == [self.photos count] - 1 ) {
        [self loadPhotos:++self.currentPage];
    }
}

Here’s body for tableView:cellForRowAtIndexPath: method. If the cell is last cell, we are first showing the loading indicator. Otherwise we are constructing the cell with current element of the photos array. Here you’ll see a strage method which has a sd_ prefix. This is not a standard method of UIImageView class. I did a small improvement to cache images locally using SDWebImage library.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    UITableViewCell *cell = nil;
    
    if (indexPath.row == [self.photos count]) {
    
        cell = [tableView dequeueReusableCellWithIdentifier:@"LoadingCell" forIndexPath:indexPath];
        UIActivityIndicatorView *activityIndicator = (UIActivityIndicatorView *)[cell.contentView viewWithTag:100];
        [activityIndicator startAnimating];
        
    } else {
        
        cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    
        NSDictionary *photoItem = self.photos[indexPath.row];
        cell.textLabel.text = [photoItem objectForKey:@"name"];
        if (![[photoItem objectForKey:@"description"] isEqual:[NSNull null]]) {
            cell.detailTextLabel.text = [photoItem objectForKey:@"description"];
        }
        
        [cell.imageView sd_setImageWithURL:[NSURL URLWithString:[photoItem objectForKey:@"image_url"]]
                          placeholderImage:[UIImage imageNamed:@"placeholder.jpg"]
                                 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                                     if (error) {
                                         NSLog(@"Error occured : %@", [error description]);
                                     }
        }];
    }
    
    return cell;
}

Server API considerations.

If your API has lot of data to return, then I suggest to use pagination. Without just returning data for requested page, it’ll be helpful to developers to if the API is returning current page, totoal pages, total items count with the response. These information will be change according to the data you have. But without maintaining lot of data locally inside phone / browser we can use server if those are included in response.

Some APIs returning only total items without returning the total number of pages. For such occasions we can calculate the total number of pages using following method. Source.

totalPages = (totalCount + pageSize - 1) / pageSize;

XCode project source code

Advertisements

Về haipro912
Đời rất dở nhưng anh vẫn phải niềm nở =))

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

w

Connecting to %s

%d bloggers like this: