Monday, May 30, 2011

iPhone Development: Accessing SOAP Services with WSDL2ObjC Part III

Has it been that long? My how time flies. Last time I wrote about WSDL2ObjC, we built a program which used a SOAP service to return us a barcode image. This time we're going to look at asynchronous calls, and we're going to build a small app which tells us what the current weather conditions are for a particular ZIP code.

Asynchronous calls are nice because we don't have to wait for them to complete. We issue the call, and then continue with whatever task we were performing. If we make a call from the same code that handles our user interface, our UI does not hang while we are waiting for completion of the call. I'm going to start this tutorial from scratch; I don't assume that you have gone through the first or second tutorials. But you can go through them, if you'd like.

A silver lining to the fact it has taken me so long to get to this third tutorial is that I will be using Apple's [relatively] new Xcode 4 to write the app.

Here's what we need:
  • WSDL2ObjC I'm using WSDL2ObjC-0.7-pre1.zip for this tutorial. You can get this from the project's home page.
  • A web service to consume. We're going to use the "Weather" service (http://wsf.cdyne.com/WeatherWS/Weather.asmx) which will, given a zip code, provide the local weather conditions for that zip code.
Let's go.

1. Generate the code 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 this demo, enter http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl
In the second field, specify a directory on your machine into which WSDL2ObjC will write the new code. When you click "Parse WSDL", you will see a few messages, one of which should say that 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 would like. In the .h file for the controller, add an action method called query, and several UITextField outlets for the fields we're going to use: zip, city, conditions, temp, humidity, and wind.

While our app is making the SOAP call, we are going to start up a UIActivityIndicatorView and tell it to spin, and when the call has completed, we're going to stop the spinner. Add another outlet called spinner, and it's of type UIActivityIndicatorView. Your .h file should look something like this:


@interface SoapWeatherViewController : UIViewController <WeatherSoapBindingResponseDelegate>
{
UITextField *zip;
UITextField *city;
UITextField *conditions;
UITextField *temp;
UITextField *humidity;
UITextField *wind;

UIActivityIndicatorView *spinner;

}

@property (nonatomic, retain) IBOutlet UITextField *zip;
@property (nonatomic, retain) IBOutlet UITextField *city;
@property (nonatomic, retain) IBOutlet UITextField *conditions;
@property (nonatomic, retain) IBOutlet UITextField *temp;
@property (nonatomic, retain) IBOutlet UITextField *humidity;
@property (nonatomic, retain) IBOutlet UITextField *wind;

@property (nonatomic, retain) IBOutlet UIActivityIndicatorView *spinner;

-(IBAction)query;

@end


Because we want our class informed when our SOAP call completes, we are going to designate it as being the delegate for the call, so we want it to implement the WeatherSoapBindingResponseDelegate protocol.

Now in the .m file, add lines to @synthesize our IBOutlets, and make sure you dealloc the UITextFields and UIActivityIndicatorView in dealloc.

Let's write the query method. This is the event that will occur when we click the "What's it like" button in the app. Let's just write boilerplate for the moment:

-(void)query { }

3. Build the UI
Double-click the XIB file for your controller to load it into Interface Builder. You will want to add a button to the NIB; title it "What's it like?" and wire its Touch Up Inside event to the query action. Add several UITextFields and labels, and connect all the outlets. And finally, add an Activity Indicator View.

It should look something like this when you're finished:


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

Take a look at Weather.h. There's a ton of stuff in there: definitions for bindings, responses, requests, etc. This code represents the data that WSDL2ObjC pulled out of Weather's WSDL.
Before compiling, we have to do two things to our build target: Double click on the project name to bring up the build settings.
  • In the Other Linker Flags section, add -lxml2 to the Linker Flags.
  • In the Other C Flags section, add -I/usr/include/libxml2

5. Write
Time to write some code. Open the .m for your controller, and #import "Weather.h". Now let's write our query method:

-(void)query
{
[zip resignFirstResponder];

WeatherSoapBinding *binding = [[Weather WeatherSoapBinding] initWithAddress:@"http://wsf.cdyne.com/WeatherWS/Weather.asmx"];
binding.logXMLInOut = YES;

Weather_GetCityWeatherByZIP *parms = [[Weather_GetCityWeatherByZIP alloc] init];
parms.ZIP = self.zip.text;

[binding GetCityWeatherByZIPAsyncUsingParameters:parms delegate:self];

[spinner startAnimating];
[parms release];
}



Zip calls resignFirstResponder so the keyboard will go away. We then create a binding and associate it with our service endpoint. We then create a request, fill in the blanks, then call the service.

The point of an asynchronous call is that we don't have to wait for it to finish. We don't have to halt our user interface, we can carry on with other tasks, etc. But we do have to tell the binding what to do when the call is finished. We give it an object which implements the WeatherSoapBindingResponseDelegate protocol (which conveniently is the controller itself [see the .h file]).

Declaring the controller as something which implements WeatherSoapBindignResponseDelegate means we have to write code for that method. Here it is
#pragma mark -
#pragma mark WeatherSoapBindingResponseDelegate methods
- (void) operation:(WeatherSoapBindingOperation *)operation completedWithResponse:(WeatherSoapBindingResponse *)response
{
[NSThread sleepForTimeInterval:5.0];

// step 1 fill in the blanks.
for (id mine in response.bodyParts)
{
 if ([mine isKindOfClass:[Weather_GetCityWeatherByZIPResponse class]])
 {
     self.city.text = [[mine GetCityWeatherByZIPResult] WeatherStationCity];
     self.conditions.text = [[mine GetCityWeatherByZIPResult] Description];
     self.temp.text = [[mine GetCityWeatherByZIPResult] Temperature];
     self.humidity.text = [[mine GetCityWeatherByZIPResult] RelativeHumidity];
     self.wind.text = [[mine GetCityWeatherByZIPResult] Wind];     
 }
}
// step 2 shut off the activity indicator, because we're done!
[spinner stopAnimating];
}

When the call completes this method is called. We sleep for five seconds (to enjoy watching the spinner), then fill in the blanks with the results of our query. Finally, we stop the activity indicator.

6. Run.
Save everything. Build and run the application. Enter your zip code into the field, then click "What's it like." You should see the spinner start spinning:
After about five seconds, you should see the weather for that location:

It's about 90, with a gentle breeze at the moment. Great weather for a motorcycle ride!

Hope this has been helpful; let me know what you think.

10 comments:

Canzhi said...

I'm pretty sure everything that's "Weather" should be replaced with "WeatherSvc"

brismith said...

Canzhi, did you have the "Add tag to service name" checked or unchecked?

YITA said...

http://wsf.cdyne.com/WeatherWS/Weather.asmx

the website seems to be crash ?

@@" ^_^|||

YITA said...

Would you send me the demo source code ?

I can't understand where to add my code ... ^_^|||

Thaks...

yita.wu55@gmail.com

YITA said...

http://wsf.cdync.com/WeatherWS/Weather.asmx?wsdl

I find that should modify "cdync" => to "cdyne".

Then connect OK!

brismith said...

You're right YITA, it should be cdyne. Thanks for letting me know.

YITA said...

Hello Brian,

Does wsdl2objc tool support WSS or HTTPS security protocol ?

Thanks!

yita.wu55@gmail.com

Vinnie D said...

Nicely done, Brian. I'm trying to connect to a JAX-WS service I've created, and this was the missing link.

I'm hoping that wsdl2objc eventually creates an option for ARC. I'll be resolving those references by hand in the near future.

As far as the tutorial, it worked like a charm!

Vinnie

jpipearagon said...

I have a problem with the following code:

CobraMovilServicePortBinding *binding =[[CobraMovilService CobraMovilServicePortBinding]initWithAddress:@"http://184.107.210.138/CobraMovilService/CobraMovilService"] ;
binding.logXMLInOut = YES;
binding.authUsername=@"xxx";
binding.authPassword=@"xxx";



CobraMovilService_getUsuarioMovil *cusername = [[CobraMovilService_getUsuarioMovil alloc] init ] ;
cusername.username=@"xxx";

[binding getUsuarioMovilAsyncUsingParameters:cusername delegate:self];

problem: [CobraMovilServicePortBinding_getUsuarioMovil setDelegate:]: unrecognized selector sent to instance 0x6b635f0


help mee...

Marco Ruiz said...

Hi brian,
I am getting this error when running this app:
Sending 'SampleViewController *' to parameter of incompatible type 'id'

this points to the line:
[binding GetCityWeatherByZIPAsyncUsingParameters:parms delegate:self];

Please help. Thank you