Wednesday, May 5, 2010

iPhone Development: Accessing SOAP Services with WSDL2ObjC

I needed to access a SOAP-based web service from the iPhone. There are a wide variety of opinions on how to go about this: "One word: Don't" was one poster's reply to a related question at Stackoverflow.com. Some people suggest writing one's own routines, while others suggest trying various code-generating tools. One I found was called gSoap, but apparently there is a fair amount of work yet to do to get it working with the iPhone. I found wsdl2objc which generates Objective-C code from a WSDL so you can call SOAP services. I want to show you how I used WSDL2ObjC to access a SOAP based web service.

Here's what you need to get started:
  • WSDL2ObjC. I'm going to use WSDL2ObjC-0.7-pre1.zip for this tutorial. You can get this from the project's home page.
  • A web service to consume. For this tutorial, we'll use the "LocalTime" service (http://www.ripedevelopment.com/webservices/LocalTime.asmx) which will, given a zip code, provide the local time for that zip code.

Let's go.

1. Generate the stubs with WSDL2ObjC.
Start WSDL2ObjC. In the first field, enter the name of the source WSDL. You can specify a local file, or a web address. For here, enter http://www.ripedevelopment.com/webservices/LocalTime.asmx?wsdl In the second field, specify a directory in which WSDL2ObjC will create the new files. You can use the Browse button to specify a directory. When you click "Parse WSDL", you will see a few messages, one of which should say it is generating Objective C Code into the output directory. Check the target directory to make sure your files are there.


2. Create the project in Xcode.
Start Xcode and create a new View-based Application. Call it anything you want. In the .h for the controller, add an action method called buttonPressed, and an outlet called field:

... UIViewController {

UITextField *field;
}

@property (nonatomic, retain) IBOutlet UITextField *field;
- (IBAction)buttonPressed:(id)sender;
@end

In the .m, add the line to @synthesize field, then add boilerplate for buttonpressed:

- (IBAction)buttonPressed:(id)sender
{
}


Save these files, then open the controller's NIB file with Interface builder. Place a text field and a button on the View. Title the button "Test" or whatever you want; stretch the field out about 3/4 the way across the view. Here's what mine looks like:



Now wire the button's Touch Up Inside event to the buttonPressed action, and connect the field's outlet to field. Save and close Interface Builder.

Go ahead and build and run, just to make sure everything's OK so far. I expect it is, but it doesn't hurt to check.

3. Pull in the WSDL2ObjC generated code.
Let's pull in the code WSDL2ObjC wrote for us: in "Other Sources," add a new Group named "LocalTime". With Finder, drag all the files that it created into the new group. Tell Xcode to copy the items into the destination folder.

Take a look at LocalTime.h. There's a ton of stuff there: definitions for bindings, responses, requests, etc. This code corresponds to the data that WSDL2ObjC pulled out of LocalTime's WSDL.

Before compiling from this point, make sure you have followed the instructions in the wiki page UsageInstructions (ie, the linker flags properties and frameworks). You find UsageInstructions at the WSDL2ObjC project home page. Incidentally, the first character of the Other C Flags property is the capital letter I as in India. You're welcome.

4. Write.
Let's write some code. Open the .m for your controller. First thing you need to do is #import "LocalTime.h" Now let's fill in the buttonPressed function. Here it is:

- (IBAction)buttonPressed:(id)sender {
  LocalTimeSoapBinding *binding = [[LocalTime LocalTimeSoapBinding] initWithAddress:@"http://www.ripedevelopment.com/webservices/LocalTime.asmx"];
  binding.logXMLInOut = YES;  // to get logging to the console.

  LocalTime_LocalTimeByZipCode *request = [[LocalTime_LocalTimeByZipCode alloc] init];
  request.ZipCode = @"29687";  // insert your zip code here.

  LocalTimeSoapBindingResponse *resp = [binding LocalTimeByZipCodeUsingParameters:request];
  for (id mine in resp.bodyParts)
  {
          if ([mine isKindOfClass:[LocalTime_LocalTimeByZipCodeResponse class]])
          {
                  field.text = [mine LocalTimeByZipCodeResult];
          }
  }
}


The first thing we do is create a binding, and associate it with ripedevelopment's web service endpoint. We then create a request object, fill in the blanks, and then call the service. The service returns a LocalTimeSoapBindingResponse, which we save in resp.

LocalTimeSoapBindingResponse has a field called bodyParts. The code runs through bodyParts, looking for the response to the call. It then stores the result in the field for us to see.

5. Run.
Save everything. Under the Run menu, bring up the Console, then build and run. Click the "Test" button on the Simulator. You should see the outbound request, a response of 200, then... Uh oh, "Unexpected response MIME type to SOAP call:text/xml"

What we're going to do here is switch the MIME type from application/soap+xml to text/xml. You can use Xcode's search and replace; you should find four occurrences of application/soap+xml Change them all to text/xml.

Now when you rerun the app, and click the "Test" button, you should see the messages in the console, and then you should see the date appear in the field.


So there you have it. Now I know I didn't show how to make an asynchronous call, nor did I talk about complex types. That's what next time is for.

Let me know what you think.