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.
where is part 3 ???
ReplyDelete