I got a question from a friend who is getting into iOS development and wanted some good practices to talk to a simple JSON API. As I was writing back, I realized my response could serve as a great post.
If you want a quick and simple library for JSON APIs, you’d do fine with
Seriously. Seriously! I’ve used it in a couple of projects already. It’s a
nice wrapper around NSURLConnection
using the block syntax and
NSOperationQueues
. It will automatically convert a response body with mime
type of application/json
into an NSDictionary
. It’s small and easy enough
to follow that you can maintain it yourself.
Here’s how you can use it. Pretend that we are building some sort of messaging
system. There’s an object, CMMessage
, that we need to populate from the data
on the server.
NSString *url = nil;
NSDictionary *headers = nil, *options = nil;
// messageId is which message we're trying to fetch
url = [NSString stringWithFormat:@"http://some-server.foo/messages/%@", messageId];
// Authenticate with a user token in a customer HTTP header
headers = [NSDictionary dictionaryWithObject:userToken forKey:@"X-User-Token"];
options = [NSDictionary dictionaryWithObject:headers forKey:kSeriouslyHeaders];
// Make the GET request with the response handler
[Seriously get:url options:options handler:^(id body, NSHTTPURLResponse *response, NSError *error) {
NSInteger statusCode = [response statusCode];
if (statusCode == 200) {
// On success, "body" is a dictionary from the JSON
// Pretend CMMessage has a class method that knows how to
// instantiate and populate from a dictionary.
CMMessage *message = [CMMessage messageFromDictionary:body];
NSLog(@"Message Subject: %@", message.subject);
}
// If we don't succeed, find out what happened and tell the
// error handler
else if (statusCode == 401) {
// Don't have access ...
}
else if (statusCode == 404) {
// Didn't find it
}
else {
// We have bigger problems
}
}];
In this example, I’m authenticating as the user with a custom HTTP header. Once
the URL and header are set up, we invoke a GET request using Seriously's
class method. It takes a block that will be invoked once the request is
complete. The body
parameter of that block is the response content.
We fetch the status code from the response
parameter and decide if it’s an
error or not. If successful (and the response mime type is JSON), then body
is now just an NSDictionary
. In this example, I create a new autoreleased
CMMessage
object using a class method that takes that dictionary to fill it
in. We now have our message we can use in the rest of the app.
It’s pretty straightforward to make PUT, POST, or other verb calls to the
server. Check the Seriously.h
header for more info.
But, before you start sprinkling code like this throughout your app, I recommend considering another layer of abstraction. We don’t want to set up the authentication headers in place every time. We want our error handling conditions to be self evident and meaningful to our application’s domain.
Let’s wrap the concept of talking to the server in it’s own object. And let’s
make a very specific method to ask for a CMMessage
. I know some of this may
seem pedantic, but bear with me.
#import "Seriously.h"
// We'll first define some simplified error codes
// that we can hand back to our error handler
typedef enum {
CMServerErrorNotFound,
CMServerErrorNotAuthorized,
CMServerErrorBiggerProblem
} CMServerErrorCode;
// Next, we'll define an error handling block that takes an error code
typedef void(^CMServerErrorHandler)(CMServerErrorCode errorCode);
// A callback block for successfully retrieving a message
typedef void(^CMServerGetMessageHandler)(CMMessage *message);
@interface CMServer : NSObject {}
// And here's the declaration for fetching a message
- (void)getMessageWithID:(NSString *)messageId
userToken:(NSString *)userToken
callback:(CMServerGetMessageHandler)callback
errorHandler:(CMServerErrorHandler)errorHandler;
@end
I’ll talk about those custom error codes in a moment.
We declare two block types that we’ll use in the method definition. The first
is the error handler. The second is specific for this “get message” method. As
I have more things to ask of the server, I add more methods and, if need be, I
add more response block types. In this case, we have a block that expects to
get passed an instantiated CMMessage
object.
Let’s jump to the implementation.
- (void)getMessageWithID:(NSString *)messageId
userToken:(NSString *)userToken
callback:(CMServerGetMessageHandler)callback
errorHandler:(CMServerErrorHandler)errorHandler
{
NSString *url = nil;
NSDictionary *headers = nil, *options = nil;
url = [NSString stringWithFormat:@"http://some-server.foo/messages/%@", messageId];
headers = [NSDictionary dictionaryWithObject:userToken forKey:@"X-User-Token"];
options = [NSDictionary dictionaryWithObject:headers forKey:kSeriouslyHeaders];
[Seriously get:url options:options handler:^(id body, NSHTTPURLResponse *response, NSError *error) {
NSInteger statusCode = [response statusCode];
if (statusCode == 200) {
CMMessage *fetchedMessage = [CMMessage messageFromDictionary:body];
// Hand back the message to the caller
callback(fetchedMessage);
}
// If we don't succeed, find out what happened and tell the
// error handler
else if (statusCode == 401) {
errorHandler(CMServerErrorNotAuthorized);
}
else if (statusCode == 404) {
errorHandler(CMServerErrorNotFound);
}
else {
errorHandler(CMServerErrorBiggerProblem);
}
}];
}
You’ll recognize most of the message body from our raw example above. But note
how the blocks are used. On success, we’ll instantiate the CMMessage
object
from the NSDictionary and then hand it in to the CMServerGetMessageHandler
callback. That’s it. This object’s work is done. What’s going to happen to that
CMMessage
? That is up to the caller now.
Also, notice how we map other status codes to our custom errors as we call the
CMServerErrorHandler
. In a more complicated application, the server may send
back a 400 error status and then more details in the body. Maybe it would be
meaningful to have a CMServerErrorMessageMoved
. It’s this object’s job to
map the conditions to the error code that makes sense to the rest of the
application.
Okay, so how do we use this?
CMServer *server = [[CMServer alloc] init];
[server getMessageWithID:messageId userToken:userToken callback:^(CMMessage * message) {
// Aha! We have a fully populated message from the server!
NSLog(@"Message Subject: %@", message.subject);
} errorHandler:^(CMServerErrorCode errorCode) {
// Oops. Something went wrong.
switch (errorCode) {
case CMServerErrorNotFound:
// ...
break;
case CMServerErrorNotAuthorized:
// ...
break;
case CMServerErrorBiggerProblem:
// raiseHell();
break;
}
}];
Ah, much cleaner. We don’t have to handle authentication headers or url generation inline anymore. Anywhere in the app that we have to get a message from the server, we can do it with a minimum of fuss. By isolating how errors are translated from the server response, your code will be cleaner and clearer to other developers. And let’s not forget that when you come back to this in 6 months you will be that “other developer”.
So, use Seriously as a library to talk JSON to your server. But consider using this pattern along with it. Isolate the code that talks to the server. Translate the responses into the domain model of your application.
✦ PermalinkMy books...