Thursday, May 4, 2017

How to Loading Large amount of Data / Images in iOS in 2 Simple Steps

If a screen/page in a mobile device  needs to load a lot of static images, that won’t have much impact on the performance of an application, because those images are stored locally on the device. The problem arises when we need to load images remotely using a URL. When loading remotely, there is a greater chance that the loading will take more time to see the screen/page.


I’ve looked on different ways to loading of Huge amount of data and images for the UI does not freeze up while the images are being loaded and for fast loading. Following are the some good ways among all the ways I’ve walkthrough.

Among different ways, there are two common ways of circumventing the technical limitations of mobile networks when dealing with large datasets.


  • Loading images with SDWebImage
  • Pagination


Loading with SDWebImage:
Now we’ll look on how images loaded from a remote location using a 3rd party Library SDWebImage


It offers a quick solution for remote loading of images and helps us to provide better performance to the user. When we use it, the images are loaded in an asynchronous manner. What this can do is show some default images to the user and load the images remotely. This will not negatively affect the performance of the app.


How it works:
With this the user will be able to see the data rather than having to wait for the images to get loaded. It can be used in various platforms, but in this post I will explain how it works in iOS. Below I will show how the images can be loaded on the iOS platform for a bookshelf view and for a list view (as seen in the images above).


Advantages of using a SDWebImage:
  • Only few lines are needed to perform this functionality.
  • Images will be displayed in visible rows automatically without delay means “Performances”.
  • An asynchronous image downloader
  • An asynchronous memory + disk image caching with automatic cache expiration handling
  • A background image decompression
  • A guarantee that the same URL won't be downloaded several times
  • A guarantee that main thread will never be blocked
  • Use GCD and ARC
To Implement loading using third party library, we need to follow certain steps:
Programming Part :
#import <SDWebImage/UIImageView+WebCache.h>
...
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
            placeholderImage:[UIImage imageNamed:@"placeholder.png"]];


By adding the above changes to our code, we will really see great performance from the application. This works really well for most of the apps where we deal with images loading remotely.
As we can see only two lines of code make great difference while loading images remotely. These are described below :
  • Import <SDWebImage/UIImageView+WebCache.h> in our file.
  • Use “sd_setImageWithURL” method that performs two functionalities it downloads image from url and during the time interval it shows default image using placeholderImage . If any case image is not able to download due to any issue it shows default image in ImageView instead of blank ImageView.


Pagination
Pagination is the classic way of doing this is to expand the scrolling area when new chunks of data are fetched.
A small POC on how to load data automatically while scrolling the table view. Data is loading over network using a rest API. So we need to 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. 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) NSMutalbeArray *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 we 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, 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 we’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 our 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, total pages, total items count with the response. These information will be change according to the data we 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.
totalPages = (totalCount + PageSize - 1) / PageSize;

At the end my conclusion is we can achieve much good result by combining these 2 above concepts. Pagination on huge amount of data fetching and loading the images from the existing chunks with SDWebImage

2 comments:

  1. I'm facing a problem related to this topic i want to load data from web service but I'm not getting an idea how to do that can u help me please?

    ReplyDelete
  2. Very informative and impressive post you have written, this is quite interesting and i have went through it completely, an upgraded information is shared, keep sharing such valuable information. Apple Developer Program

    ReplyDelete