iOS app Design Blog – How to get JSON data from REST API?

UPDATE:

Many readers, while appreciating the need for the tutorial, made me aware that NSURLConnection has aged enough since iOS 9. While I agree, this is not quite the case yet. NSURLSession is a great grand umbrella under which everything that earlier resided under NSURLConnection – will move.

Considering so many newbie devs confused about which one to use, I have updated the code base to also use NSURLSession.

Verdict: NSURLSession or NSURLConnection?

NSURLConnection, though deprecated, isn’t going anywhere soon. Mac OS library still has it. However, if you want to use per request session configuration settings (cache, credential policies etc) – NSURLSession is better candidate. NSURLConnection will still work for the purpose at hand – but going down the line it is likely to be unsupported.

OK, read on now.

Most iOS apps today rely upon displaying data to user from a server. There are obvious advantages of this approach over displaying it from local app data source:

  • Limited local storage
  • Maintaining data updates through server controlled by developer
  • No need to release app updates with respect to data change

JSON is the most popular data format considering the support it enjoys with modern programming languages. So our today’s blog is dedicated to designing how you can GET ANY JSON data from any REST API endpoint.

As part of this tutorial, we will build an objective-C class that can be reused through out your app. This class – let’s call it APIDataFetcher:

  • will be a singleton / only hold class methods so instantiation does not matter.
  • will fetch data from given REST URL (supplied by caller function – typically this would be your UI, but it can also be a background queue that does this via a non-UI thread)
  • will report errors to the caller

So let’s build it from scratch.

API Data Fetcher – what it contains:

Nothing. Since all it provides are static functions, there are no class members. It will have to expose these functions though. For now, let’s assume

  • it has functions named loadDataFromAPI and loadDataFromAPIUsingSession
  • loadDataFromAPI or loadDataFromAPIUsingSession calls a (success) handler block (that the caller provides) with JSON data it fetched from the API
  • loadDataFromAPI or loadDataFromAPIUsingSession calls a (failure) handler block (again, the caller provided one) with the error it received from the API in case something goes wrong

Cut short to the .h file:

#import <Foundation/Foundation.h>

typedef void (^SuccessBlock)(id result);
typedef void (^FailureBlock)(NSError * error);

@interface APIDataFetcher : NSObject
+ (void) loadDataFromAPI : (NSString *) url : (SuccessBlock) successBlock :(FailureBlock) failureBlock;
+ (void) loadDataFromAPIUsingSession : (NSString *) url : (SuccessBlock) successBlock :(FailureBlock) failureBlock;
@end

And that’s that. Let’s move on to the implementation.

API Data Fetcher – what would it additionally need:

Though no instance members, APIDataFetcher.m would need some everlasting static objects to do its job.  They are:

  • an NSOperationQueue object – this is necessary because multiple REST API request from your app can be serialized and tracked effectively using this class. Serialized and tracked effectively – What? You don’t get it? That’s why it exists – you don’t have to care a bit about that phrase when you use NSOperationQueue, and this is not the only scenario where it’s useful. There are plenty of times in iOS app design where an operation queue is inevitable, and we will keep visiting it often.
  • a success block – static one. Definition inside the header.   this is removed due to potential bug in multiple request use case.
  • a failure block – static one. Again, the definition inside the header.  – this is removed due to potential bug in multiple request use case.

So now what? Fire – straight from the hell.

API Data Fetcher – how it’s implemented:

Straight to the code for APIDataFetcher.m file:

//  APIDataFetcher.m
//
//  Created by Nirav Bhatt on 9/5/15.
//  Copyright (c) 2015 IphoneGameZone. All rights reserved.
/* Generic JSON fetch routines through NSURLConnection
 Part of: https://github.com/vividcode/iOSAPIDataApp/tree/master/iOSAPIDataApp*/

#import "APIDataFetcher.h"

static NSOperationQueue * _connectionQueue = nil;
static NSURLSession * _session = nil;

@implementation APIDataFetcher

+ (NSOperationQueue *) connectionQueue
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (!_connectionQueue)
        {
            _connectionQueue = [[NSOperationQueue alloc] init];
        }
    });
    return _connectionQueue;
}

+ (void) createURLSession
{
    static dispatch_once_t onceToken;
    
    if (!_session)
    {
        dispatch_once(&onceToken, ^{
            _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        });
    }
}

+ (void) loadDataFromAPIUsingSession : (NSString *) url : (SuccessBlock) successBlock :(FailureBlock) failureBlock
{
    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    
    [self createURLSession];
    
    NSURLSessionDataTask * task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
    {
        if (response != nil)
        {
            if ([[self acceptableStatusCodes] containsIndex:[(NSHTTPURLResponse *)response statusCode]])
            {
                if ([data length] > 0)
                {
                    NSError *jsonError  = nil;
                    id jsonObject  = nil;
                    
                    jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
                    
                    if (jsonObject != nil)
                    {
                        [self presentData:jsonObject :successBlock];
                    }
                    else
                    {
                        [self presentError:jsonError :failureBlock];
                    }
                }
                else
                {
                    [self presentError:nil :failureBlock];
                }
            }
            else
            {
                [self presentError:nil :failureBlock];
            }
        }
        else
        {
            [self presentError:error :failureBlock];
        }
    }];
    
    [task resume];
}

+ (void) loadDataFromAPI : (NSString *) url : (SuccessBlock) successBlock :(FailureBlock) failureBlock
{
    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    [NSURLConnection sendAsynchronousRequest:request queue:[self connectionQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
     {
         if (response != nil)
         {
             if ([[self acceptableStatusCodes] containsIndex:[(NSHTTPURLResponse *)response statusCode] ])
             {
                 if ([data length] > 0)
                 {
                     NSError *jsonError  = nil;
                     id jsonObject  = nil;
                     
                     jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
                     
                     if (jsonObject != nil)
                     {
                         [self presentData:jsonObject :successBlock];
                     }
                     else
                     {
                         [self presentError:jsonError :failureBlock];
                     }
                 }
                 else
                 {
                     [self presentError:nil :failureBlock];
                 }
             }
             else
             {
                 [self presentError:nil :failureBlock];
             }
         }
         else
         {
             [self presentError:connectionError :failureBlock];
         }
     }];
}

+ (NSIndexSet *) acceptableStatusCodes
{
    return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 99)];
}

+ (void) presentData:(id)jsonObject :(SuccessBlock) block
{
    [[NSOperationQueue mainQueue] addOperationWithBlock:
     ^{
         block(jsonObject);
     }];
}

+ (void) presentError:(NSError *)error :(FailureBlock) block
{
    [[NSOperationQueue mainQueue] addOperationWithBlock:
     ^{
         block(error);
     }];
}
@end

And now let’s look at the main functions one by one.

The first one – connectionQueue (which is only applicable for loadDataFromAPI which uses NSURLConnection)-  returns a static singletone instance of type NSOperationQueue, which is quite obvious, because you don’t want to create & recreate your NSOperationQueue during the lifetime of your app. Single _connectionQueue instance should be able to keep track of all your REST API request. Again, none of much of your concern.

The next one, loadDataFromAPI (or loadDataFromAPIUsingSession), is the heart of this post.  It accepts the REST API URL to fetch the data from. In addition to that, it expects two execution blocks – a success block and a failure block – to be invoked in the context of view controllers who called these functions – for data update and error respectively.

Do not forget that these are the very things given to loadDataFromAPI from it’s caller, mostly the view controller – view controller expects data, or in the worst case, some error info, and successBlock and failureBlock stack variables are our only way to give it back. APIDataFetcher does not, and must not know anything about it’s callers.

  • Create NSURLRequest object from the URL passed (in case of POST request, this request object must additionally be initialized with payload data, but for now, let’s only worry about GET request)
  • If using loadDataFromAPI, Invoke [NSURLConnection sendAsynchronousRequest] which fires the REST API GET request. If URL and everything else is correct with the REST API, this function comes back with a valid NSURLResponse object, along with some NSData. If not, it comes back with NSError object. In any case, completionHandler of sendAsynchronousRequest  marks the end of the URL request, and you have the task of notifying your caller what you got.
  • If using loadDataFromAPIUsingSession, create NSURLSession using default configuration first. Notice the use of dispatch_once_t token to ensure multiple requests do not recreate the session. This session will then create NSURLSessionDataTask with URL request as in above step. You need to resume this task in order for things to set off. Everything else is common to loadDataFromAPI or loadDataFromAPIUsingSession.
  • Inside completionHandler, we perform checks of received NSData, and convert it to id type JSON var using function [NSJSONSerialization JSONObjectWithData]. This function gives a JSON object from the data only if the data is valid JSON. If not, it returns nil, and you may want to notify caller about the error.
  • Helper function acceptableStatusCodes tells if the received NSURLResponse belongs to valid Http status codes, depending upon which you may want to perform specific user notification tasks.

  • Helper function presentError does the simple task: Invoking _failureBlock block to notify caller of the error. However, an important thing to note is that it does this using [NSOperationQueue mainQueue] – the queue using the UI thread of your app.
  • Helper function presentData does exactly the same thing as presentError – except that it does it with the JSON object by invoking the _successBlock.

And we are done. Get all the code for APIDataFetcher, use it with your view controllers, and play with your REST APIs forever!

Tagged with:

Leave a Reply

Your email address will not be published. Required fields are marked *

*