Monday, November 18, 2013

Create your own web service for an iOS app, part two

This installment in our iOS development series details how to connect to the web service you created and start fetching data.

In part one of our series on how to create the web service backend for the iGlobe app, we created the web database, the web service backend, and the iOS frontend (Storyboard). Now we'll focus on connecting to the web service and fetching actual data. This tutorial will work in iOS 6 or iOS 7.

Step 4: Fetch data

a. NSURLConnection

First, let's add some properties to our UsersListViewController.m:

@interface UsersListViewController () 
@property (nonatomic, strong) NSMutableArray *testArray;
@property (nonatomic, strong) NSMutableData *buffer;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView  *spinner;
@property (nonatomic, strong) NSURLConnection *myConnection;
 
We're going to use NSURLConnection to create a web connection and fetch data, so let's change our viewDidLoad method to look like this:

- (void)viewDidLoad{
    [super viewDidLoad];
    // Animate the spinner
    [self.spinner startAnimating];
    // Create the URL & URLRequest
    NSURL *myURL = [NSURL URLWithString:@"http://www.yourserver.com/iglobe/getusers.php"];
    NSURLRequest *myRequest = [NSURLRequest requestWithURL:myURL];
    // Create the connection
    self.myConnection = [NSURLConnection connectionWithRequest:myRequest delegate:self];
    //Test to make sure the connection worked
    if (self.myConnection){
        self.buffer = [NSMutableData data];
        [self.myConnection start];
    }else{
        NSLog(@"Connection Failed");
    }
}
 
Now NSURLConenction has four delegate methods you must implement. Be sure to add the to our @interface line for UsersListViewController.h (or in .m -- just make sure it's the @interface line and not the @implementation line). Let's add the first method:

# pragma NSURLConnection Methods

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    
    [self.buffer setLength:0];
}
 
This method is called when the app receives a response from the server. We'll simply reset the buffer length. Now we must deal with each time the app receives data from the server:

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    
    [self.buffer appendData:data];
    
}
 
We append the received data to the existing buffer data.  Let's handle any error response from the server as well:

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    // Do cleanup
    self.myConnection = nil;
    self.buffer     = nil;
    
    // Inform the user, most likely in a UIAlert
    NSLog(@"Connection failed! Error - %@ %@",
          [error localizedDescription],
          [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
 
Next, implement the method that is called when the connection finishes loading data from the web server:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"Succeeded!");
    //Create a queue and dispatch the parsing of the data
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // Parse the data from JSON to an array
        NSError *error = nil;
        NSArray *jsonString = [NSJSONSerialization JSONObjectWithData:_buffer 
options:NSJSONReadingMutableContainers error:&error];
        
        // Return to the main queue to handle the data & UI
        dispatch_async(dispatch_get_main_queue(), ^{
            
            //Check if error or not
            if (!error) {
                //If no error then PROCESS ARRAY
                self.testArray = [[NSMutableArray alloc] initWithCapacity:50];
                for (NSDictionary *tempDictionary in jsonString) { 
 // Extract each dictionary’s username & put it into our array
                    [self.testArray addObject:[tempDictionary objectForKey:@"username"]];
                }
                // Call reload in order to refresh the tableview
                [self.tableView reloadData];

            }else{
                NSLog(@"ERROR %@", [error localizedDescription]);
            }
            
            //Stop animating the spinner
            [self.spinner stopAnimating];
            
            // Do cleanup
            self.myConnection = nil;
            self.buffer     = nil;
        });

    });
}
 
The important bit is that we've seen the web response logged in our console, thus we know it's an array. We also know the array has dictionaries at each index; therefore, we must loop or iterate through each NSDictionary entry in the array and fetch the "username" key's value or object. We add that object to our self.testArray with each new iteration. In the end, we refresh our tableview to use the newly populated self.testArray with our usernames.

Great, so we can read information from our webservice. As we will see later, reading more complex data from the webservice is just a matter of creating a more complex request string on the iOS side and putting it together with some logic in the php server side. This is one way to fetch data, using NSURLConnection directly in a viewDidLoad. It's better than calling NSURLConnection on the main thread, but we want to make sure our code is re-usable, particularly the web fetch code since it's probably code we will want to use again in future projects. That's the reason we created the SantiappsHelper class.

The Users class is a container for our individual players. Its interface looks like this:

#import 


@interface Users : NSObject {
	
}
@property (nonatomic,copy) NSString *userName;
@property (nonatomic,copy) NSString *userPoints;

-(id)initWithUserName:(NSString*)userName userPoints:(NSString*)userPoints;
@end 
and its very simple implementation looks like this:
#import "Users.h"


@implementation Users


-(id)initWithUserName:(NSString*)nameOfUser userPoints:(NSString*)userPoints;
{
	if ( (self = [super init]) == nil )
        return nil;
	self.userName = nameOfUser;
	self.userPoints = userPoints;
	return self;
}

@end

b. GCD and completion blocks

We want to make our code a bit neater and more portable; we'll achieve this by creating our SantiappsHelper Class. This helper class will concentrate on fetching data from the web. It's very similar to other class files you have worked with before, but basically it has only Class Methods.

#import 
#import "Tag.h"

typedef void (^Handler)(NSArray *users);
typedef void (^Handler2)(NSArray *points);
typedef void (^Handler3)(NSArray *usersPointsArray);

@interface SantiappsHelper : NSObject {
}

+(void)fetchUsersWithCompletionHandler:(Handler)handler;

+(void)fetchPointForUsersArray:(NSArray*)usersArray WithCompletionHandler:(Handler3)handler;

+ (BOOL)postNewTag:(Tag*)passingObject;// from gamebumpconnector

@end
 
These three methods do the following: (1) fetchUsersWithCompletionHandler will fetch the list of users in the game, (2) fetchPointForUsersArray: WithCompletionHandler: will fetch the points for all users, and (3) postNewTag will create a new location tag for a particular user.

The idea is that the user will exchange tokens or tags with another user. Physically the two users at the same location will bump phones to initiate a tag worth two points. As the data is exchanged, each user account will post a 2-point value tag to the database. Originally the game was built to create tags individually at locations, but you can't very well call it a game of tag if you don't tag a second user, can you? 

Let's review what these methods do.

// THIS METHOD FETCHES USER ARRAY
+(void)fetchUsersWithCompletionHandler:(Handler)handler {
	
    NSString *urlString = [NSString stringWithFormat:@"http://www.myserver.com/myApp/getusers.php"];
    NSURL *url = [NSURL URLWithString:urlString];
    
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:
NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10];
    
    [request setHTTPMethod: @"GET"];

    __block NSArray *usersArray = [[NSArray alloc] init];

    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        // Peform the request
        NSURLResponse *response;
        NSError *error = nil;
        NSData *receivedData = [NSURLConnection sendSynchronousRequest:request
                                                     returningResponse:&response
                                                                 error:&error];
        if (error) {
            // Deal with your error
            if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
                NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
                NSLog(@"HTTP Error: %d %@", httpResponse.statusCode, error);
                return;
            }
            NSLog(@"Error %@", error);
            return;
        }
        
        NSString *responseString = [[NSString alloc] initWithData:receivedData encoding:
NSUTF8StringEncoding];
        //NSLog(@"responseString fetchUsers %@", responseString);
        
        usersArray = [NSJSONSerialization JSONObjectWithData:
[responseString dataUsingEncoding:NSASCIIStringEncoding] options:0 error:nil];

        //Returns handler
        if (handler) {
            dispatch_async(dispatch_get_main_queue(), ^{
                handler(usersArray);
            });
        }
    });
}

//Fetches points for users array
+(void)fetchPointForUsersArray:(NSArray*)usersArray WithCompletionHandler:(Handler3)handler{
    NSError *error = nil;
    
    NSData *data = [NSJSONSerialization dataWithJSONObject:usersArray options:0 error:&error];
    
    if (error)
        NSLog(@"%s: JSON encode error: %@", __FUNCTION__, error);
    
    // create the request
    NSURL *url = [NSURL URLWithString:@"http:/ /www.myserver.com/myApp/readpointsforarray.php"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"POST"];
    [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:data];
    
    __block NSArray *pointsArray = [[NSArray alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        // Peform the request
        NSURLResponse *response;
        NSError *error = nil;
        
        // issue the request
        NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:
&response error:&error];
        
        if (error) {
            // Deal with your error
            if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
                NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
                NSLog(@"HTTP Error: %d %@", httpResponse.statusCode, error);
                //return;
            }
            NSLog(@"Error %@", error);
            //return;
        }
        
        NSString *responseString = [[NSString alloc] initWithData:returnData encoding:NSUTF8StringEncoding];
        //NSLog(@"asyncrhonous: %@",responseString);
        
        pointsArray = [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:
NSASCIIStringEncoding] options:0 error:nil];
        //NSLog(@"pointsArray %@", pointsArray);
        
        if (handler){
            dispatch_async(dispatch_get_main_queue(), ^{
                handler(pointsArray);
            });
        }
    });
}

// Called from MKViewController, creates shared tags points=2
+ (BOOL)postNewTag:(Tag*)passingObject{
    //1.  Log the tag for verification first
	NSLog(@"passingObject:%@,%@,%@,%@,%@",passingObject.sender, 
passingObject.receiver, passingObject.rglatitude, passingObject.rglongitude, passingObject.rgcountry);
	//NSLog(@"tagReceived:%@,%@,%@,%@",tagReceived.originUdid, 
tagReceived.destintyUdid, tagReceived.rglatitude, tagReceived.rglongitude);
 
	//2.REBUILD status string from passingObject
	NSString *s1 = [[NSString alloc] initWithFormat:@"sender=%@&latitude=%@&longitude=%@&country=%
@&receiver=%@&points=2",passingObject.sender,passingObject.rglatitude,passingObject.rglongitude,
passingObject.rgcountry,passingObject.receiver]; 
 
 //3.  Post tag to cloud
    NSData *postData = [s1 dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
    NSString *postLength = [NSString stringWithFormat:@"%d", [postData length]];
    NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] init] autorelease];
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@
"http:/ /www.myserver.com/myApp/writephp.php"]];
    [request setURL:url];
    [request setHTTPMethod:@"POST"];
    [request setValue:postLength forHTTPHeaderField:@"Content-Length"];
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:postData];
    
    NSURLResponse *response;
    NSError *error;
	// We should probably be parsing the data returned by this call, for now just check the error.
    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
    NSLog(@"success!");
	[s1 release];
    return (error == nil);
}
fetchUsersWithCompletionHandler:
At the beginning of our helper class, we defined three typedefs; this just means we're defining a type, which we called Handler. We'll see in a minute what these are used for. Basically, this method creates a GET type request, which will be used to GET our users from the web service we created. It creates a block array to be used inside the block to store our returned data. Before executing the block (which contains the NSURLConnection method call), we dispatch the operation to the background queue and execute it there. This means that a slow or busy server will not block our main thread. The main thread is responsible for drawing operations and user interaction. If a heavy-duty operation such as fetching data from a web server or processing images and video were to be run on the main thread, the user would not be able to interact with the app until that task was complete.

The handler is the typedef that is created once the method has completed; therefore, we check to see if it exists. Once it does exist, we call back to the main thread, returning the now populated usersArray. That usersArray will be used to fill our list of users in our tableview.
fetchPointForUsersArray: WithCompletionHandler

This method is responsible for sending in an array of users and fetching their points back. It talks to the last php file we created and returns the usersArray, which contains a dictionary with users and their points.

postNewTag
The final method is in charge of logging a newly created tag and then posting it to the database.

We have one method for writing data to the database (postNewTag) and two methods for reading data from the database (fetchUsers… and fetchPointForUsersArray…). We read and write points (or Tags) and read users. We must have a way to write users; we'll cover this later in a class called ModalViewController, which will accept a login from a user to create a new user.  

Before we bump phones and exchange tags we must be able to create tags, so let's create a Tag Class and its ViewController and plot them on the map view. Once we have that, we will save our user information to our app and be ready to bump.

In the next installment of this series, we'll look into creating tags and storing them so we can exchange them.

1 comments:

Appreciate your concern ...