Chapter 4. Integrating iOS Applications into Enterprise Services
In the early days of software development, you ran your programs directly on a computer, sitting in front of the console. Later, the idea of timesharing and remote sessions came into being, bringing with it the rise of the 3270 and VT-100 terminals, along with punch cards and paper tape. Later, network computing became the rage, with Sun famously proclaiming that the network was the computer. We got RPC, CORBA, and we’ve evolved today into SOAP, REST, and AJAX. But no matter what it’s called, or what format the data moves in, all these technologies attempt to solve the same problem, the same one that has existed since client-server architectures first came upon the earth.
As an iOS developer, you face the same problem today. You need to be able to reliably and (hopefully) easily integrate user-facing UI with all the messy business logic, persistence, security, and magic unicorn blood that does all the hard work on the backend.[1]
The Rules of the Road
There are basically two possible scenarios when integrating iOS applications into Enterprise services. Either you are starting from scratch on both ends, or you have some legacy service with which you need to ingratiate your new application. The former situation is vastly preferable, because you can fine-tune your protocols and payloads to the demands of mobile clients, but the later scenario is probably the more likely one. In any event, I’ll talk in general about each of them, and then look at some specific techniques for dealing with various protocols and payloads under iOS.
Rule 1: Insist on Contract-Driven Development
Modern IDEs and service toolkits can take a lot of the grunt work out of creating service interfaces. They can auto-generate classes for you, create testing frameworks, generate documentation, and I wouldn’t be surprised if they could prepare world-class sushi on demand by now. But there’s one thing that they can do that is the bane of every client developer on the planet, and that’s to auto-generate the interface from the backing classes. Java-based SOAP development is getting to be semi-notorious for this. On paper, it’s great. Just sprinkle magic annotations in your source code, and tools such as CXF will figure it all out, generate the WSDL files, and you’re good to go.
Unfortunately, in reality, you end up with a couple of problems. For one, you give up control over what the SOAP structures (or RESTful XML structures) are going to look like. I have seen some unforgivably butt-ugly XML produced by class-driven interface development. But more seriously, it means that “innocent” changes made to the underlying Java classes can have unforeseen repercussions on the payload, causing mysterious failures. There’s nothing like doing your first regression test against a new server release where “nothing changed,” and finding out that the payloads have been fundamentally altered by a trivial change in the server-side classes.
Because of this, I consider it almost a necessity that both sides of the house adhere to a contract-first approach to APIs. TDD is great for this: you can write some SOAPUI tests to exercise the specified contract, which will fail until the service is implemented. This keeps the server-side developers honest, because the acceptance test for their service is whether or not it passes the SOAPUI tests, and later on down the road, it will make sure they don’t break the API contract.
Rule 2: Be Neither Chunky Nor Chatty
A friend of mine, web services guru Anne Thomas Manes of Burton Group, likes to say that web services should be chunky, not chatty. That is to say, it’s better to send down too much information in a single request, rather than make dozens of calls to get the data, paying the latency toll on each request.
In general, I agree with her, especially since in a 3G (or worse) mobile environment, the latency can get quite painful. But on mobile devices, keeping down the payload size can be just as important. Not only do many users pay by the byte, but you have much more restrictive memory profiles on mobile devices. Thankfully, this isn’t as much of a factor on an iOS device, because there tends to be plenty of memory available to the foreground process, but on Android or J2ME devices, available memory can be quite limited. If you suck down a 4MB XML payload as a string, and then parse it into an XML DOM, you could have 8MB of memory being consumed just by the data.
Of course, if you’re only developing the protocol for iOS consumption, you only need to worry about bankrupting your users when they get their data plan bill, but these days, no one develops backend services for a single mobile platform, so you may have to live with constraints imposed by the needs of lesser devices. In a perfect world, try to design your protocols to allow for both chunky and chatty requests, and let the client decide how much data they want to consume at once.
First Things First: Getting a Connection
I’m going to make a general assumption for the body of this chapter, which is that whatever you are talking to, you’re doing it via HTTP (or HTTPS). If you’re doing something custom using sockets, the 1990s are calling and they would like their architecture back. In all seriousness, though, almost all client-server mobile work these days is done using HTTP for transport.[2]
The iOS SDK comes with a perfectly adequate HTTP implementation, found in the CFNetwork framework. But anyone who uses it is a masochist, because it makes you do so much of the work yourself. Until recently, I would have recommended using the ASIHttpRequest package. ASI stands for All Seeing Interactive (or “eye”), by the way, so if you happen to be an Illuminati conspiracist, this may not be the software for you. Why did I like this package so much?
It is licensed under the über-flexible BSD license, so you can do pretty much anything you want with it, and it provides a lot of the messy housekeeping code that HTTP requests tend to involve. Specifically, it will automatically handle:
Easy access to request and response headers |
Authentication support |
Cookie support |
GZIP payload support |
Background request handling in iOS 4 and later |
Persistent connections |
Synchronous and asynchronous requests |
(And more!) |
However, after the early access version of this book was released, I
learned that ASIHttpRequest is in
danger of becoming an orphan project. This is especially disturbing,
because it means it may never be converted over to ARC, among other
things. It would be more disturbing if it were the only game in town;
until recently, it was, because the system-provided alternative,
NSURLConnection
, didn’t support asynchronous
requests. Being able to choose synchronous and asynchronous requests is
critically important. Almost all of the time, you want to use asynchronous
requests, even if the request seems synchronous. For
example, a login may seem synchronous. After all, you can’t really do
anything further until the request completes. But in reality, if you do a
real synchronous HTTP request, everything locks up (including activity
indicators) until the response
returns. The pattern you want to use is to lock the UI (typically by
setting various UI elements to have
userInteractionEnabled
set false), and then make an
asynchronous call with both the success and failure callbacks re-enabling
the UI.
Thankfully, in iOS 5, the system-provided class was extended to include asynchronous requests, so there’s no longer a reason to avoid using it.
Using NSURLConnection—The BuggyWhip News Network
Let’s see this package put through its paces, in this case by
creating a page in the BuggyWhipChat
application that lets you see the latest news on buggy whips.
Unfortunately, the server folks haven’t gotten the news feed working yet,
so we’ll be using CNN for testing. The page itself is going to be simple,
a new button on the iPad toolbar that gets the CNN home page, and displays
the HTML in an alert. Yes, this is a totally useless example, but we are
talking about BuggyWhipCo here! Actually, the reason that we’re not doing
more with the HTML is two-fold: there’s a perfectly good
UIWebView
control that already does this, and the point
is to highlight the NSURLConnection
package, not to
display web pages.
First, we add the toolbar item to the detail XIB and hook it up the “Sent Action” for the button to a new IBAction called showNews in the view controller, as shown in Example 4-1.
- (void)handleError:(NSString *)error{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Buggy Whip News Error" message: error delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; [alert show]; } -(IBAction) showNews:(id) sender { NSURL *url = [NSURL URLWithString:@"http://www.cnn.com/index.html"]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if (error != nil) { [self handleError:[error localizedDescription]]; } else { [self displayResponse:data]; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpresp = (NSHTTPURLResponse *) response; if ([httpresp statusCode] == 404) { [self handleError:@"Page not found!"]; } else { [self displayResponse:data]; } } } }]; }
So, what’s going on here? Most of the action takes place in the
showNews
method, which is triggered
by the button push of the new tab bar item. When it is pressed, the code
creates an NSURL
reference to the CNN website, and then creates an
NSURLRequest
HttpRequest
instance using the requestWithURL
method. There are a number of
parameters you can set on the returned object, some of which I cover
later, but by default, you get an HTTP GET request. Next, we use the
sendAsynchronousRequest
method of the
NSURLConnection
class to actually perform the
request. This method is handed three parameters: the request we just
created, the NSOperationQueue
to run the request on
(which, for thread safety, will usually be the current queue), and a block
which serves as the completion handler (that is, the handler that deals
with successful or failed calls).
If the request fails catastrophically (connection failures, server
failures, etc.), the failure is returned in the error parameter, which
will be non-null. If the call succeeds, which can include 401
(unauthorized) and 404 (not found) errors, the call response is populated
along with the body of the response as an NSData
object. You can proceed to process the response body, returned in the data
parameter.
If we run the application and click on the tab bar button, we get the expected results, shown in Figure 4-1.
If we change the hardwired string that we use to create the URL to something bogus, such as http://www.cnnbuggywhip.com, we get an alert message with the error shown in Figure 4-2.
But suppose we specify a valid host name, but an invalid address, such as http://www.cnn.com/buggywhipco.html? As I mentioned, a 404 error is considered a success and will call the success selector, leading to something like the alert in Figure 4-3.
What we really want to have happen here is to display a meaningful alert message, something that we could internationalize, for example. So, we need to look a bit more carefully at allegedly “good” responses, leading to the rewritten method shown in Example 4-2.
-(IBAction) showNews:(id) sender { NSURL *url = [NSURL URLWithString:@"http://www.cnn.com/index.html"]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if (error != nil) { [self handleError:[error localizedDescription]]; } else { if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpresp = (NSHTTPURLResponse *) response; if ([httpresp statusCode] == 404) { [self handleError:@"Page not found!"]; } else { [self displayResponse:data]; } } } }]; }
What has happened here is that we have checked to see that the
response is really an NSHTTPURLResponse
object
(which it should be for any HTTP request). If so, you can cast the
response object to that type, and use the statusCode
method to check that you haven’t
gotten a 404, 401, or other HTTP protocol error.
Something a Little More Practical—Parsing XML Response
Now that we’ve had a brief introduction to using
NSURLRequest
, we can use it to do something a bit
more useful than spitting out raw HTML in alerts. We still don’t have any
web services from the backend developers, so we’ll use a publicly
available RESTful web service provided by the US National Weather Service.
Given an address such as:
http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdXMLclient.php?zipCodeList=20910+25414&product=time-series&begin=2004-01-01T00:00:00&end=2013-04-21T00:00:00&maxt=maxt&mint=mint
We can get back XML that looks like the sample shown in Example 4-3.
<dwml xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:noNamespaceSchemaLocation=\ "http://www.nws.noaa.gov/forecasts/xml/DWMLgen/\ schemaDWML.xsd"> <head> <product srsName="WGS 1984" concise-name="time-series" operational-mode="official"> <title> NOAA's National Weather Service Forecast Data </title> <field>meteorological</field> <category>forecast</category> <creation-date refresh-frequency="PT1H"> 2011-07-25T00:30:56Z </creation-date> </product> <source> <more-information> http://www.nws.noaa.gov/forecasts/xml/ </more-information> <production-center> Meteorological Development Laboratory <sub-center> Product Generation Branch </sub-center> </production-center> <disclaimer> http://www.nws.noaa.gov/disclaimer.html </disclaimer> <credit> http://www.weather.gov/ </credit> <credit-logo> http://www.weather.gov/images/xml_logo.gif </credit-logo> <feedback> http://www.weather.gov/feedback.php </feedback> </source> </head> <data> <location> <location-key>point1</location-key> <point latitude="42.90" longitude="-71.29"/> </location> <moreWeatherInformation applicable-location="point1"> http://forecast.weather.gov/MapClick.php?\ textField1=42.90&textField2=-71.29 </moreWeatherInformation> <time-layout time-coordinate="local" summarization="none"> <layout-key>k-p24h-n1-1</layout-key> <start-valid-time> 2011-07-26T08:00:00-04:00 </start-valid-time> <end-valid-time> 2011-07-26T20:00:00-04:00 </end-valid-time> </time-layout> <time-layout time-coordinate="local" summarization="none"> <layout-key>k-p24h-n1-2</layout-key> <start-valid-time> 2011-07-26T20:00:00-04:00 </start-valid-time> <end-valid-time> 2011-07-27T09:00:00-04:00 </end-valid-time> </time-layout> <parameters applicable-location="point1"> <temperature type="maximum" units="Fahrenheit" time-layout="k-p24h-n1-1"> <name>Daily Maximum Temperature</name> <value>82</value> </temperature> <temperature type="minimum" units="Fahrenheit" time-layout="k-p24h-n1-2"> <name>Daily Minimum Temperature</name> <value>61</value> </temperature> </parameters> </data> </dwml>
There’s a mouthful of data in that payload, but essentially, given a zip code and a start and end date, the service will return a list of high and low temperature forecasts that lie between those dates. Let’s use this service to create a more interesting alert.
We can start by writing a utility function to generate the correct
endpoint for the REST request, given a series of parameters. By creating a
stand-alone generator, it will be easier to unit test that logic. So, we
create a WebServiceEndpointGenerator
class, with a
static method to create the appropriate endpoint, which looks like Example 4-4.
+(NSURL *) getForecastWebServiceEndpoint:(NSString *) zipcode startDate:(NSDate *) startDate endDate:(NSDate *) endDate { NSDateFormatter *df = [NSDateFormatter new]; [df setDateFormat:@"YYYY-MM-dd'T'hh:mm:ss"]; NSString *url = [NSString stringWithFormat: @"http://www.weather.gov/forecasts/xml/\ sample_products/browser_interface/\ ndfdXMLclient.php?zipCodeList=%@\ &product=time-series&begin=%@\ &end=%@&maxt=maxt&mint=mint", zipcode, [df stringFromDate:startDate], [df stringFromDate:endDate]]; [df release]; return [NSURL URLWithString:url]; }
Back on our detail page, we add another bar button hooked up to a
new selector, called showWeather
. To
save some space here, and because we’re interested in the backend code,
not the UI treatment, we’ll have the success method for this call simply
send the output to the log using NSLog
. But how to extract the useful data? We
could use string functions to find the pieces of the XML we were
interested in, but that’s laborious and not very robust. We could use the
XML pull parser that comes with iOS, but pull parsers are also a bit of a
pain, and I tend to avoid using them unless I need to significantly parse
down the in-memory footprint of the data. So, since iOS doesn’t have a
native DOM parser, we’ll dig one out of the open source treasure
chest.
You actually have your choice of packages, TouchXML and KissXML. I prefer KissXML, because it can both parse XML to a DOM, and generate XML from a DOM, whereas TouchXML can only parse. KissXML is available at: http://code.google.com/p/kissxml. Integration largely involves copying files into the project, adding the libxml2 library, and one additional step: adding a directory to the include search path. The KissXML Wiki has all the details on how to add the package to your project.
I’ll note here that KissXML is, at the moment, not compatible with the ARC compiler. For that reason, I highly recommend making it into a static library that you include in your main application, as described in Chapter 2.
With the library in place, we can use XPath to pluck just the data we want out of the returned payload from the web service; the code is shown in Example 4-5.
-(id) getSingleStringValue:(DDXMLElement *) element xpath:(NSString *) xpath { NSError *error = nil; NSArray *vals = [element nodesForXPath:xpath error:&error]; if (error != nil) { return nil; } if ([vals count] != 1) { return nil; } DDXMLElement *val = [vals objectAtIndex:0]; return [val stringValue]; } - (void)gotWeather:(NSData *)data { UIAlertView *alert; NSError *error = nil; DDXMLDocument *ddDoc = [[DDXMLDocument alloc] initWithXMLString:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] options:0 error:&error]; if (error == nil) { NSArray *timelayouts = [ddDoc nodesForXPath:@"//data/time-layout" error:&error]; NSMutableDictionary *timeDict = [NSMutableDictionary new]; for (DDXMLElement *timenode in timelayouts) { NSString *key = [self getSingleStringValue:timenode xpath:@"layout-key"]; if (key != nil) { NSArray *dates = [timenode nodesForXPath:@"start-valid-time" error:&error]; NSMutableArray *dateArray = [NSMutableArray new]; for (DDXMLElement *date in dates) { [dateArray addObject:[date stringValue]]; } [timeDict setObject:dateArray forKey:key]; } } NSArray *temps = [ddDoc nodesForXPath:@"//parameters/temperature" error:&error]; for (DDXMLElement *tempnode in temps) { NSString *type = [self getSingleStringValue:tempnode xpath:@"@type"]; NSString *units = [self getSingleStringValue:tempnode xpath:@"@units"]; NSString *timeLayout = [self getSingleStringValue:tempnode xpath:@"@time-layout"]; NSString *name = [self getSingleStringValue:tempnode xpath:@"name"]; NSArray *values = [tempnode nodesForXPath:@"value" error:&error]; int i = 0; NSArray *times = [timeDict valueForKey:timeLayout]; for (DDXMLElement *value in values) { NSString *val = [value stringValue]; NSLog(@"Type: %@, Units: %@, Time: %@", type, units, [times objectAtIndex:i]); NSLog(@"Name: %@, Value: %@", name, val); NSLog(@" "); i++; } } return; } alert = [[UIAlertView alloc] initWithTitle:@"Error parsing XML" message: [error localizedDescription] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; [alert show]; } - (IBAction)showWeather:(id)sender { NSURL *url = [WebServiceEndpointGenerator getForecastWebServiceEndpoint:@"03038" startDate:[NSDate date] endDate:[[NSDate date] dateByAddingTimeInterval:3600*24*2]]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if (error != nil) { [self handleError:error.description]; } else { [self gotWeather:data]; } }]; }
Let’s take a good look at what’s going on here. First off, we have
the new handler for the weather button we added to the tab bar, called
showWeather
. What this method does is
to construct an endpoint using the generator we just created. It passes in
the zip code for Derry, NH (my home sweet home), and start and end dates
that correspond to now and 24 hours from now. Then it does an asynchronous
request to that URL using the same completion handler mechanism that we
used in the previous example.
One advantage that the
NSURLConnection
method has over my previous choice,
ASI
HttpRequest
, is that it is purely
block-driven for the handlers. This can be significant when you have
multiple asynchronous requests in flight, because a block-based handler
lets you tie the handler directly to the request, rather than using
selectors to specify the delegates to handler the responses.
The interesting bit, from an XML parsing perspective, is what
happens inside the gotWeather
method
when the request succeeds. After doing the normal checks for bad status
codes, it uses the initWithXMLString
method of the DDXMLDocument
class to parse the
returned string into an XML DOM structure. The method takes a pointer to
an NSError
object as an argument, and that is what
you should check after the call to see if the parse was successful.
Assuming the parse succeeded, the next step is to use the nodesForXPath
method on the returned XML
document, specifying //data/time-layout
as the XPath to look
for. This method returns an array of DDXMLElement
objects, or nil
if nothing matched the XPath specified. The
weather XML we get back follows a pattern I find particularly obnoxious in
XML DTDs: it makes you match up data from one part of the payload with
data from another part. In this case, the time-layout
elements have to be matched up with the attributes in the
temperature
elements, so we iterate over the
time-layout
elements, building a dictionary of the layout
names to the dates they represent.
I find when working with KissXML that I frequently want to get the
string value of a child node that I know only has a single instance. So I
usually write a helper method such as the getSingleStringValue
method used in this
example. Using it, I can snatch the keys and start dates, and populate the
dictionary with them. This method is shown in Example 4-6.
-(id) getSingleStringValue:(DDXMLElement *) element xpath:(NSString *) xpath { NSError *error = nil; NSArray *vals = [element nodesForXPath:xpath error:&error]; if (error != nil) { return nil; } if ([vals count] != 1) { return nil; } DDXMLElement *val = [vals objectAtIndex:0]; return [val stringValue]; }
Basically, the method takes the root element for the extraction, and a relative or absolute XPath expressing as a string. It gets the array of matching elements, makes sure there wasn’t an error (such as an invalid XPath string), and if it gets back exactly one element in the array, returns it.
Once I have the dictionary, I iterate over the temperature data, grabbing attributes and element values as appropriate, then use the diction to find the time associated with the reading. Finally, I dump the data out to the log, resulting in this kind of output:
Type: maximum, Units: Fahrenheit 2011-11-16T07:00:00-05:00, Daily Maximum Temperature = 58 2011-11-17T07:00:00-05:00, Daily Maximum Temperature = 49 2011-11-18T07:00:00-05:00, Daily Maximum Temperature = 48 Type: minimum, Units: Fahrenheit 2011-11-16T19:00:00-05:00, Daily Minimum Temperature = 42 2011-11-17T19:00:00-05:00, Daily Minimum Temperature = 27
Generating XML for Submission to Services
Whereas you almost always want to use a parsed DOM to extract data from a service, how you construct XML to send back to a server is more flexible. The particular web service we used in the last example is GET-only, so we’ll use a mythical service that takes a POST for this one. Let’s say that the server crew has finally gotten their act together, and you can now post a news item by sending the following XML:
<newsitem postdate="2011-07-01" posttime="14:54"> <subject> Buggy Whips Now Available in Brown </subject> <body> Buggy whips are the hottest thing around these days, and now they're available in a new designer color, caramel brown! </body> </newsitem>
If you wanted to construct payloads that follow this format, and post them up to the server, you could use the code snippet in Example 4-7.
-(NSString *) constructNewsItemXMLByFormatWithSubject:(NSString *) subject body:(NSString *) body { NSDate *now = [NSDate date]; NSDateFormatter *df = [NSDateFormatter new]; [df setDateFormat:@"YYYY-MM-dd"]; NSDateFormatter *tf = [NSDateFormatter new]; [tf setDateFormat:@"hh:mm"]; NSString *xml = [NSString stringWithFormat:@"<newsitem\ postdate=\"%@\" posttime=\"%@\">\ <subject>%@<subject><body>%@</body></newsitem>", [df stringFromDate:now], [tf stringFromDate:now], subject, body]; return xml; } - (IBAction)sendNews:(id)sender { NSLog(@"By Format: %@", [self constructNewsItemXMLByFormatWithSubject: @"This is a test subject" body:@"Buggy whips are cool, aren't they?"]); }
If we hook the sendNews
IBAction
up to a new bar button item and run the app,
we can see that perfectly reasonable XML is produced:
By Format: <newsitem postdate="2011-07-26" posttime="10:42"> \ <subject>This is a test subject<subject>\ <body>Buggy whips are cool, aren't they?</body></newsitem>
Let’s compare this with the corresponding code, done using DOM construction techniques (Example 4-8).
#import "DDXMLElementAdditions.h" . . . -(NSString *) constructNewsItemXMLByDOMWithSubject: (NSString *) subject body:(NSString *) body { NSDate *now = [NSDate date]; NSDateFormatter *df = [NSDateFormatter new]; [df setDateFormat:@"YYYY-MM-dd"]; NSDateFormatter *tf = [NSDateFormatter new]; [tf setDateFormat:@"hh:mm"]; DDXMLElement *postdate = [DDXMLElement attributeWithName:@"postdate" stringValue:[df stringFromDate:now]]; DDXMLElement *posttime = [DDXMLElement attributeWithName:@"posttime" stringValue:[tf stringFromDate:now]]; NSArray *attributes = [NSArray arrayWithObjects:postdate, posttime, nil]; DDXMLElement *subjectNode = [DDXMLElement elementWithName:@"subject" stringValue:subject]; DDXMLElement *bodyNode = [DDXMLElement elementWithName:@"body" stringValue:body]; NSArray *children = [NSArray arrayWithObjects:subjectNode, bodyNode, nil]; DDXMLElement *doc = [DDXMLElement elementWithName:@"newsitem" children:children attributes:attributes]; NSString *xml = [doc XMLString]; return xml; }
This version, if passed the same data as we used before, produces identical XML. So why go to the trouble of building up all those DOM structures when a simple format will do? Well, one reason is that using DOM makes sure that you don’t leave out a quotation mark or angle bracket, or forget an end tag, producing bad XML.
But there’s a much more important reason, which is the same reason you never use format strings to create SQL statements, it’s too easy to inject garbage into the payload. Let’s compare the two methods again, but with slightly more complex data (newlines and indents have been added to improve readability):
By Format: <newsitem postdate="2011-07-26" posttime="11:23"> <subject>This is a test subject<subject> <body> Buggy whips are cool & neat, > all the rest, aren't they? </body> </newsitem> By DOM: <newsitem postdate="2011-07-26" posttime="11:23"> <subject>This is a test subject</subject> <body> Buggy whips are cool & neat, > all the rest, aren't they? </body> </newsitem>
Oh my, look at all the illegal XML characters in the middle of our payload, just waiting to break any innocent XML parser waiting on the other end. By contrast, the example using DOM construction has appropriately escaped any dangerous content.
The decision as to whether to use formatting or DOM construction isn’t absolute. If you have good control of the data you’re sending, and can be absolutely sure that no XML-invalid characters will be contained inside the parameters, formatting can be a real time- and code-saver. But it is inherently more dangerous than using a DOM, and you also have to deal with pesky problems such as escaping all your quote signs. I’ve used both techniques where appropriate, but I’m starting to lean more heavily toward DOM construction these days.
One last item: once you have the XML, how do you send it in a POST? The code is fairly simple; it’s almost identical to the NSURLConnection code to do a GET. An example can be seen in Example 4-9.
-(void) sendNewsToServer:(NSString *) payload { NSURL *url = [NSURL URLWithString:@"http://thisis.a.bogus/url"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:[request allHTTPHeaderFields]]; [headers setValue:@"text/xml" forKey:@"Content-Type"]; [request setAllHTTPHeaderFields:headers]; [request setTimeoutInterval:30]; NSData *body = [payload dataUsingEncoding:NSUTF8StringEncoding]; [request setHTTPBody:[NSMutableData dataWithData:body]]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if (error != nil) { [self handleError:error.description]; } else { [self displayResponse:data]; } }]; }
The only additional bits we need to do are to call setHTTPBody
, add a content type using setAllHTTPHeaderFields
, and use setHTTPMethod
to indicate this is a POST. In
all other ways, this acts just like a normal request, including the block
handler and how response data is handled.
Once More, with JSON
Now that you know the basics of how to get XML back and forth to servers, it’s time to move on something a little more 21st century. One side effect of the rise of HTML5, AJAX, CSS, and all the other cool web technologies is that there are now a bunch of client-friendly JSON-speaking services out there that your application can leverage.
JSON is definitely the new kid on the block in the enterprise, and large companies are just starting to embrace it (my company has been using it in some of our newer products for about a year now). The big advantages that JSON brings are a lightweight syntax and easy integration into JavaScript. But how about iOS?
Again, until recently, I would have recommended a third-party library. There’s a perfectly serviceable third party library out there, called SBJSON. You can find it at https://github.com/stig/json-framework/ and once again, the site provides detailed instructions on how to install the package into your application. You’ll still need it if iOS 4 compatibility is an issue to you, but if you can make iOS 5 your minimum version, you can use the JSON support that’s now integrated into iOS.
We already have an HTTP connection framework in place, in the form
of the NSURLConnection
class, so all we’re really
doing is changing how we generate and parse our payloads. Once again, our
web service team is off taking long lunches, so we’ll have to test against
a public service, in this case the GeoNames geographical names database.
The GeoNames API takes all of its parameters on the URL, and returns a
JSON payload as the response. So for the request
http://api.geonames.org/postalCodeLookupJSON?postalcode=03038&country=AT&username=buggywhipco
,
you’ll get back a payload that looks like:
{"postalcodes": [ { "adminName2":"Rockingham", "adminCode2":"015", "postalcode":"03038", "adminCode1":"NH", "countryCode":"US", "lng":-71.301971, "placeName":"Derry", "lat":42.887404, "adminName1":"New Hampshire" } ] }
Basically, it’s an array of postal code records, with a name of fields inside it. To see how we can access this data in iOS, we’ll add another button to the tab bar, and add the code shown in Example 4-10 to the controller.
- (IBAction)lookupZipCode:(id)sender { NSURL *url = [WebServiceEndpointGenerator getZipCodeEndpointForZipCode:@"03038" country:@"US" username:@"buggywhipco"]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if (error != nil) { [self handleError:error.description]; } else { [self gotZipCode:data]; } }]; } - (void)gotZipCode:(NSData *)data { NSError *error; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (error != nil) { [self handleError:error.description]; return; } NSArray *postalCodes = [json valueForKey:@"postalcodes"]; if ([postalCodes count] == 0) { NSLog(@"No postal codes found for requested code"); return; } for (NSDictionary *postalCode in postalCodes) { NSLog(@"Postal Code: %@", [postalCode valueForKey:@"postalcode"]); NSLog(@"City: %@", [postalCode valueForKey:@"placeName"]); NSLog(@"County: %@", [postalCode valueForKey:@"adminName2"]); NSLog(@"State: %@", [postalCode valueForKey:@"adminName1"]); NSString *latitudeString = [postalCode valueForKey:@"lat"]; NSString *northSouth = @"N"; if ([latitudeString characterAtIndex:0]== '-') { northSouth = @"S"; latitudeString = [latitudeString substringFromIndex:1]; } float latitude = [latitudeString floatValue]; NSLog(@"Latitude: %4.2f%@",latitude, northSouth); NSString *longitudeString = [postalCode valueForKey:@"lng"]; NSString *eastWest = @"E"; if ([longitudeString characterAtIndex:0]== '-') { eastWest = @"W"; longitudeString = [longitudeString substringFromIndex:1]; } float longitude = [longitudeString floatValue]; NSLog(@"Longitude: %4.2f%@", longitude, eastWest); } }
So, what have we got going on here? The request side is pretty
straightforward: we generate a URL endpoint (the generator does pretty
much what you’d expect it to do), and submit the request. When we get a
response back, we check for error, and then use a method in the
NSJSONSerialization
class called JSONObjectWithData
to parse the JSON.
The iOS 5 JSON implementation uses dictionaries, arrays, and strings
to store the parsed JSON values. We look for the
postalcodes
value in the top level dictionary, which
should contain an NSArray
of postal codes. If we find
it, we iterate over the array, and pluck out the individual values,
printing them to the log. There’s even a little extra code to turn the
latitude and longitude into more human-readable versions with E/W and N/S
values.
There’s only one problem with the code as currently written: it will
throw an exception when you run it. If you look closely at the original
JSON, you’ll see that’s because the lat
and
lng
values don’t have double quotes around them. As a
result, the objects that will be put into the dictionary for them will be
NSDecimalNumber
objects, not
NSString
. Thus, all the string manipulation code
will fail. We can fix the problem easily enough:
NSDecimalNumber *latitudeNum = [postalCode valueForKey:@"lat"]; float latitude = [latitudeNum floatValue]; NSString *northSouth = @"N"; if (latitude < 0) { northSouth = @"S"; latitude = - latitude; } NSLog(@"Latitude: %4.2f%@", latitude, northSouth); NSDecimalNumber *longitudeNum = [postalCode valueForKey:@"lng"]; float longitude = [longitudeNum floatValue]; NSString *eastWest = @"E"; if (longitude < 0) { eastWest = @"W"; longitude = - longitude; } NSLog(@"Longitude: %4.2f%@", longitude, eastWest);
The reason that I made that deliberate mistake is to emphasize a point about JSON serialization, which is that you have to look closely at the shape of the JSON that gets returned, and know what to expect at any particular point in the tree. Running the corrected code generates good data in the log:
BuggyWhipChat[9811:b603] Postal Code: 03038 BuggyWhipChat[9811:b603] City: Derry BuggyWhipChat[9811:b603] County: Rockingham BuggyWhipChat[9811:b603] State: New Hampshire BuggyWhipChat[9811:b603] Latitude: 42.89N BuggyWhipChat[9811:b603] Longitude: 71.30W
Having to generate JSON is somewhat less frequent a task, but it’s
equally simple to do. Simply use NSDictionary
,
NSArray
, NSString
,
NSDecimalNumber
and so on to construct the payload
that you wish to send, then use the corollary method dataWithJSONObject
of the
NSJSONSerialization
class, which will return the
JSON NSData
that corresponds to the
structure.
SOAP on a Rope
So far, we’ve been dealing with modern, fairly light-weight web service protocols. Alas, in many enterprise companies, SOAP is still the name of the game. SOAP has a big advantage in that you can take a WSDL file and (theoretically) generate client bindings in a variety of languages and operating systems, and they will all be able to successfully communicate with the server.
The reality is somewhat less rosy. Things are certainly better than they were a few years ago, when getting a Java client to talk to an ASP.NET SOAP server could drive you insane. These days, the impedance mismatches of data types and incomplete data bindings are largely a thing of the past. What hasn’t changed is the God-awful bindings that you can end up with, especially if you annotate classes and let the framework generate the WSDL, as is the habit in many projects. And who can blame them, since WSDLs are one of the most unpleasant file specifications to author?
The reality of life, however, is that there’s a good chance you may need to consume a SOAP server as part of an iOS application. Luckily, there’s good tooling available to help. Just as there is wsdl2java to create Java bindings for SOAP, there’s a tool called WSDL2ObjC that will create all the classes to bind iOS applications. You can download it from http://code.google.com/p/wsdl2objc/, and unlike the other tools we’ve discussed in this section, it’s not a library but a Mac utility. When you download the tool and run it, you are presented with a window with two text fields and a button (Figure 4-4). You put a URI specification for a WSDL file in the first text field, the second specifies the directory that you want your generated code to be placed into. When you click the button, you get a directory full of files that you can add to your iOS project, and use to call the web service.
In this example, we’re consuming another weather service, because we really care about the weather here at BuggyWhipCo. An extra-humid day can ruin an entire batch of lacquered buggy whips, you know! In any event, we have the WSDL URI for a SOAP-based weather service, and we’ve used it to generate a whole bunch of files that support the binding to the service. If you look in the directory you generated your files into, you’ll see something like this:
NSDate+ISO8601Parsing.h NSDate+ISO8601Parsing.m NSDate+ISO8601Unparsing.h NSDate+ISO8601Unparsing.m USAdditions.h USAdditions.m USGlobals.h USGlobals.m WeatherSvc.h WeatherSvc.m xsd.h xsd.m
With the exception of the two WeatherSvc files, everything else is standard and common to all SOAP web services you generate, and if you have more than one service you bind to, you’ll only need one copy of those files (if you generate all your services into the same directory, this should happen automatically).
Well, that was certainly easy! Except, actually, we’re just getting started. As anyone who has ever used SOAP bindings knows, making them work involves a bunch of guesswork and a menagerie of intermediate objects of no particularly obvious purpose. Let’s look at the code that you’d need to write in order to consume this service, shown in Example 4-11.
- (IBAction)showSOAPWeather:(id)sender { WeatherSoapBinding *binding = [WeatherSvc WeatherSoapBinding]; binding.logXMLInOut = YES; WeatherSvc_GetWeather *params = [[WeatherSvc_GetWeather alloc] init]; params.City = @"Derry, NH"; WeatherSoapBindingResponse *response = [binding GetWeatherUsingParameters:params]; for (id bodyPart in response.bodyParts) { if ([bodyPart isKindOfClass:[SOAPFault class]]) { NSLog(@"Got error: %@", ((SOAPFault *)bodyPart).simpleFaultString); continue; } if ([bodyPart isKindOfClass: [WeatherSvc_GetWeatherResponse class]]) { WeatherSvc_GetWeatherResponse *response = bodyPart; NSLog(@"Forecast: %@", response.GetWeatherResult); } } }
The general pattern here, as with most SOAP bindings, is that you
get a binding object (which has all the information about things such as
the URI of the service). Then you get a request object, set all the
parameters of the request, then call the appropriate method on the binding
(in this case, GetWeatherUsingParameters
), handing in the
request. There are two versions of each request method, one synchronous
(the one we’re using in this example), and one asynchronous (which uses
the same kind of callback mechanism we’ve used in the JSON and XML
examples). We also set the logXMLInOut
flag to true, so
we’ll get a look at what we’re sending and receiving in pure SOAP form,
this can be very helpful figuring out where data is hiding in the
response.
When the response returns, you have to iterate over the body parts, checking for SOAP faults. If the body part isn’t a SOAP fault, you have to cast it to the appropriate object. And here’s where the fun begins, because there’s nothing to tell you what the possible types are, except for diving through the generated code looking for where the elements are populated. But, if you do it all right, you’ll get results.
2011-11-18 08:16:17.365 BuggyWhipChat[69775:fb03] Forecast: Sunny
Looks like nice beach weather! If you’re getting the feeling that I’m not a big fan of SOAP, you wouldn’t be wrong. And, mind you, what we just saw is an extremely simple SOAP call, with no authentication and very simple request and response objects. I have not tried to make WSDL2ObjC work with WS-Security, although there is anecdotal evidence that it is possible. In any event, the good news is that SOAP on iOS isn’t impossible.
A Final Caution
Whenever you are relying on third-party libraries, you’re at the mercy of the maintainers. If it is a proprietary library, you have to hope that they will keep the tool up to date, and that it will not be broken by the next update to XCode or iOS.
For open source libraries, things are a little better. If the committers on the project aren’t willing or able to update the project, you have the source and can take a swing at it yourself. Depending on the license, this may mean that you have to make the sources to your changes available for download.
This is not some abstract warning, I know for a fact that most of the tools mentioned in the chapter do not work in projects that are using the Automatic Reference Count compiler feature in iOS 5. This means that you’re going to be faced with the choice of trying to get the libraries working with ARC, or building them as non-ARC static libraries that are called from your ARC project. One factor to consider when choosing libraries is how active the developer community is, and how often new versions come out. WSDL2ObjC, for example, seems relatively moribund, and might slip into orphan status in the future.
With our application cheerfully (if uselessly) talking to web services, we should think about how we’re going to test it. That’s what you’ll learn in Chapter 5.
[1] Note: Magic Unicorn Blood contains chemicals known to the state of California to cause cancer, birth defects, or other reproductive harm. Please consider the use of Magic Pixie Dust in place of Magic Unicorn Blood when feasible.
[2] Yes, I know, technically HTTP is the Application Layer, according to the OSI model. My apologies to pedantic taxonomists.
Get Developing Enterprise iOS Applications now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.