Posts Tagged ‘cocoa’

JSON-RPC in Objective-C

Friday, November 6th, 2009

Handling JSON in Objective-C is already pretty easy with the excellent strict parser provided by json-framework. However, interacting with a web service in a graphical application can be hard to get right. Frequently unresponsive or unpredictable behavior is a big turnoff for users. Recently I have been developing a lot for the iPhone and more often than not, ideas that beget these applications revolve around central web applications. Many client-server interactions usually boil down to the familiar CRUD apps we’re all so familiar generating with Rails and Django but require much more effort.

Writing applications for a mobile device introduces several challenges: limited memory, slow network and a language and framework designed without an eye on modern agile web services. Recently while writing an iPhone application with heavy web interaction I got lazy doing all that work and wrote DeferredKit – a port of Twisted’s Deferred with many extensions for us Objective-C coders. DeferredKit makes asynchronous programming in Objective-C really easy with very little overhead and includes an asynchronous JSON-RPC client. It allows you to write code like this:

  - (void)doRegistration {
    [[[[[DKDeferred jsonService:SERVICE_URL]
       myApp] register:array_(self.username, self.password)] 
      addCallback:callbackTS(self, doRegistrationCompleted:)]
     addErrback:callbackTS(self, doRegistrationFailed:)];
  }
 
  - (id)doRegistrationCompleted:(id)result {
    // do something with result
    return result;
  }
 
  - (id)doRegistrationFailed:(NSError *)err {
    // do something with error
    return err;
  }

Jumping In

Getting started with DeferredKit is relatively easy. Download the or pull DeferredKit from github and drop everything from DeferredKit/CocoaDeferred/Source into your project (you can also compile DeferredKit as a direct dependency in Xcode, a how-to is in the README).

First, a bit of explanation. Despite the recent addition of blocks to the Objective-C language, the blocks runtime is not available to us iPhone developers (or anybody who wants to deploy to targets older than 10.6, sadly). Also, since there is no native function object in Objective-C, functions can not be passed around as first-class citizens, stored in collection types, persisted, etc. DeferredKit provides one called DKCallback. You will almost never have to touch the interface of DKCallback since handy macros are provided to generate function objects.

A deferred is defined as an object that encapsulates a return value that is not yet available (meaning, it will be returned from the network when done downloading, or return from a thread when done computing). The deferred then dispatches the value to a series of callbacks as soon as it becomes available.

JSON-RPC is exposed in DeferredKit through a class called DKJSONServiceProxy which employs a bit of magic to enable the dynamic method calling pattern you saw above. For convenience, autorelease constructors are also provided in DKDeferred:

  id service = [DKDeferred jsonService:@"http://myservice.net/json/" name:@"myapp.sayHello"];

(note: don’t forget to #import DKDeferred+JSON.h)

Calling a method returns a deferred:

  DKDeferred *d = [service :args_array];

To which you must add at least one callback for the call to be performed. In this case, callbackTS builds a callback with a target (can be any object), and a selector to be performed. A callback could also be a function pointer, NSInvocation or any object conforming to the DKCallback protocol.

  [d addCallback:callbackTS(self, gotSayHelloResults:)];

In case your JSON-RPC server returns an error (via the error key) or encounters a network error or parse error, deferred will automatically call any errbacks, or “error callbacks,” added with an NSError object.

  [d addErrback:callbackTS(self, handleSayHelloError:)];

Callbacks and errbacks are written as a function or method which takes a single argument, the result and returns id. JSON-RPC results will always be an NSDictionary with a result key, among others.

  - (id)gotSayHelloResults:(id)results {
    self.helloLabel.text = [results objectForKey:@"result"];
    return nil;
  }

Errbacks are the same as callbacks except they will always be called with an NSError argument. The userDict dictionary will be populated with the error dictionary produced by your JSON-RPC server. userDict will be different for different error types however, so be sure to check which kind of error is being returned.

  - (id)handleSayHelloError:(NSError *)err {
    [self alertError:[[[error userDict] objectForKey:@"error"] objectForKey:@"message"]];
    return nil;
  }

Conclusion

I’ve only had the opportunity to touch on the basics of DeferredKit, but a post is coming soon with more details. It is a tested, lightweight framework for writing asynchronous code and can greatly improve your productivity when working with web services.

Compressing NSData objects using zlib

Monday, November 2nd, 2009

This entirely iPhone friendly tutorial highlights how easy it is to compress our familiar NSData objects using zlib – the most commonly used compression library on the planet. This is the same library that compresses the PNGs and PDFs, ZIPs and GZIPed files that make up your daily life.

The zlib c library, which is bundled with all Mac OS X and iPhone 2.0+ devices offers an extensive but non obtrusive API for compressing data. The most basic functions are the stream API which serve nicely when, for instance, dealing with a large files, or matching a specific compression requirement. The method presented here is one which is utility provided by the atop the streaming API. Readers familiar with python will recognize this if ever you’ve had to handle data with the zlib module.

First, Allocate a data structure of type const ByteF containing the bytes in our uncompressed NSData structure.

  const Bytef *inBytes = (const Bytef *)[uncompressedData bytes];

Allocate enough data to start compressing data. The zlib manual recommends allocating 1% more than the length of your uncompressed data, plus twelve bytes. This gives zlib enough room to start compressing data, resizing the output buffer as needed.

  uLongf outLength = ([uncompressedData length] * 1.1) + 12;
  Bytef *outBytes = (Bytef *)malloc(outLength);

Now simply call compress:

  int z_result = compress(outBytes, &outLength, inBytes, [uncompressedData length]);

compress will return one of three constants. To handle errors, check for the return value. Depending on your situation, you could return an NSError. In this case, I return an empty NSData and check for return value length.

  NSData *ret;
  if (z_result == Z_OK) {
    ret = [NSData dataWithBytesNoCopy:outBytes length:outLength freeWhenDone:YES];
  } else {
    ret = [NSData data];
    switch (z_result) {
      case Z_MEM_ERROR:
        printf("compressData got Z_MEM_ERROR out of memory. :(");
      case Z_BUF_ERROR:
        printf("compressData got Z_BUF_ERROR output buffer wasn't larege enough :(");
      default:
        break;
    }
  }
}

compressData function

NSData* compressData(NSData *uncompressedData) {
  if ([uncompressedData length] == 0) 
    return uncompressedData;
  const Bytef *inBytes = (const Bytef *)[uncompressedData bytes];
  uLongf outLength = ([uncompressedData length] * 1.1) + 12;
  Bytef *outBytes = (Bytef *)malloc(outLength);
  int z_result = compress(outBytes, &outLength, inBytes, [uncompressedData length]);
  NSData *ret;
  if (z_result == Z_OK) {
    ret = [NSData dataWithBytesNoCopy:outBytes length:outLength freeWhenDone:YES];
  } else {
    ret = [NSData data];
    switch (z_result) {
      case Z_MEM_ERROR:
        printf("compressData got Z_MEM_ERROR out of memory. :(");
      case Z_BUF_ERROR:
        printf("compressData got Z_BUF_ERROR output buffer wasn't larege enough :(");
      default:
        break;
    }
  }
  return ret;
}

compressData that returns an NSError.

#define ZLIB_COMPRESS_DOMAIN @"zlib_compress_domain"
 
NSError* compressData2(NSData *inData, NSData **outData) {
  if ([inData length] == 0) {
    *outData = inData;
    return nil;
  }
  const Bytef *inBytes = (const Bytef *)[inData bytes];
  uLongf outLength = ([inData length] * 1.1) + 12;
  Bytef *outBytes = (Bytef *)malloc(outLength);
  int z_result = compress(outBytes, &outLength, inBytes, [inData length]); 
  if (z_result == Z_OK) {
    *outData = [NSData dataWithBytesNoCopy:outBytes length:outLength freeWhenDone:YES];
    free(outBytes);
    return nil;
  }
  return [NSError errorWithDomain:ZLIB_COMPRESS_DOMAIN 
    code:z_result userInfo:[NSDictionary dictionary]];
}

NSData category

@interface NSData (CompressionAdditions)
 
- (NSData *)compress;
- (NSData *)compress:(NSError **)errorOut;
 
@end
 
 
@implementation NSData (CompressionAdditions)
 
- (NSData *)compress { 
  return compressData(self);
}
 
- (NSData *)compress:(NSError **)errorOut {
  NSData *ret;
  *errorOut = compressData2(self, &ret);
  return ret;
}
 
@end