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.

50 comments:

Rik said...

You had to switch to "text/xml" because you used the SOAP 1.1 version of the methods. Since the WSDL allows for both SOAP 1.1 and 1.2, the wsdl2objc generator assumes you'll be using SOAP 1.2.

So instead of switching the header text to "text/xml" and forcing SOAP 1.1, you can directly use the SOAP 1.2 version of the binding, i.e. LocalTimeSoap12Binding instead of LocalTimeSoapBinding.

Gary said...

I am following your example and i keep getting the following errors after putting in the button pressed code:

: error: 'LocalTimeSoapBinding' undeclared (first use in this function)

error: 'binding' undeclared (first use in this function)

warning: 'LocalTime' may not respond to '+LocalTimeSoapBinding'

warning: (Messages without a matching method signature

error: 'LocalTimeSoapBindingResponse' undeclared (first use in this function)

error: 'resp' undeclared (first use in this function)


error: expression does not have a valid object type

brismith said...

Gary, when WSDL2OBJC built the code for you, did you click the button "Add tag to service name"? If so, regenerate the code, but leave that unchecked. It adds the letters "Svc" after the service name. Or take a look in the generated files: If you have files named LocalTimeSvc instead of LocalTime, then that's what happened.

brismith said...

Gary, one more thing: did you add #import "LocalTime.h" to the beginning of your implementation file?

Sagi said...

Hello, I'm having the same issue as Gary.
I did not use the "tag" checkbox, I have #import "LocalTime.h" in my view controller's implementation file.
It seems LocalTimeSoapBinding is not declared in any of the files generated by wsdl2objc. I'm using wsdl2objc 0.6 though.

wonton said...

I am using SDK iPHone 4.0. I will not compile the
wsdl2objectc at all!

Ling

wonton said...

OK, I figure it out.

Good tutorial!

D.R. said...

When I press the button, it gives this error message and nothing displays in the text field.

ResponseError:
Error Domain=LocalTimeSoapBindingResponseHTTP Code=1 UserInfo=0xf28c90 "Unexpected response MIME type to SOAP call:text/xml"

brismith said...

D.R., did you replace application/soap+xml with text/xml?

Theresa said...

thanks for sharing, now i am going to do as per your instructions . i will let you know result. iphone application development

david said...

I can compile with SDK 4.0

I don't have methode called myWebServiceBinding,
but I guess a can use the methode myWebService instead.

but I get a soap Error anyway.

Yan Chen said...

Great tutorial! What shall I do if I need to login first, if login is OK, then perform data entry, Like amazon mobile?

brismith said...

Yan Chen,

I suppose that depends on the behavior of the SOAP service. The result of a login might be a success code or token which would be used for subsequent queries; it all depends on the SOAP service you are querying.

Johnny said...

Thanks for a great tutorial.

Johnny said...

Thanks for a great tutorial. I tried Rik's suggestion to use LocalTimeSoap12Binding but I could not get that approach to work.

Sean said...

You need to use LocalTimeSoap12, instead of LocalTimeSoapBinding. But I run into another problem, I get a whole lot of the xml related errors:


Undefined symbols:
"_xmlSearchNs", referenced from:
-[LocalTime_LocalTimeByZipCodeResponse xmlNodeForDoc:elementName:] in LocalTime.o
-[LocalTime_LocalTimeByZipCode xmlNodeForDoc:elementName:] in LocalTime.o
-[LocalTime_LocalTimeByZipCodeResponse deserializeElementsFromNode:] in LocalTime.o
-[LocalTime_LocalTimeByZipCode deserializeElementsFromNode:] in LocalTime.o
"_xmlNewNsProp", referenced from:
-[LocalTimeSoap12_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
-[LocalTimeSoap_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
"_xmlAddChild", referenced from:
-[LocalTime_LocalTimeByZipCodeResponse addElementsToNode:] in LocalTime.o
-[LocalTime_LocalTimeByZipCode addElementsToNode:] in LocalTime.o
-[LocalTimeSoap12_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
-[LocalTimeSoap12_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
-[LocalTimeSoap12_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
-[LocalTimeSoap12_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
-[LocalTimeSoap_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
-[LocalTimeSoap_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
-[LocalTimeSoap_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
-[LocalTimeSoap_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o
"_xmlNewDocNode", referenced from:
-[LocalTime_LocalTimeByZipCodeResponse xmlNodeForDoc:elementName:] in LocalTime.o
-[LocalTime_LocalTimeByZipCode xmlNodeForDoc:elementName:] in LocalTime.o
-[LocalTimeSoap12_envelope serializedFormUsingHeaderElements:bodyElements:] in LocalTime.o


Anyone?

brismith said...

Sean, you probably need to find "Other Linker Flags" and add the -lxml2 to the Linker Flags, then in the GCC 4.2 - Code Generation section, find "Other C Flags" and add -I/usr/include/libxml2 to Other C Flags.

Sean said...

brismith, right on, that did the trick!! I should have read the instructions more carefully, Brain did mention it..

OK, now, when I press the Test button, nothing happens... any tips?

brismith said...

Sean, a couple of things come to mind: open the console while running the test and see whether there are any error messages. Barring that, you might want to confirm that you've connected the outlet properly.

Sean said...

dOh~~ text/xml thing, newB shame! Thx so much man!

Sam said...
This comment has been removed by the author.
Shrey Shrivastava said...
This comment has been removed by the author.
Shrey Shrivastava said...

m getting 571 errors. :((

it is saying

libxml/tree.h no such directory or structure

brismith said...

Shrey, did you add "-lxml2" to "Linker Flags" and add "-I/usr/include/libxml2" to "Other C Flags" in the GCC 4.2 - Code Generation section.

udhaya said...

i'm getting 271 errors and 72 warnings.. like Expected ")" before xmlChar, xmlNodePtr, xmlDocPtr etc..

vinh said...

Hi, thank you for your instruction. It's really helpful. I can build and go successful with your tutorial. But I can't implement it with my SOAP. Please help me. My SOAP: http://demo.qgs.vn/spa/webservices/wsdl/basic

Thanks so much.

brismith said...

Udhaya, you probably forgot to add -lxml2 to Linker Flags, and -I/usr/include/libxml2 to Other C Flags

brismith said...

Vinh,

When I ran this code:

BasicServiceBinding *binding = [[BasicService BasicServiceBinding] initWithAddress:@"http://demo.qgs.vn/spa/webservices/call/basic"];
binding.logXMLInOut=YES;

BasicServiceBindingResponse *resp = [binding list_servicesUsingFacility_id: [NSNumber numberWithInt:0]];

I got the response
?xml version="1.0" encoding="UTF-8"?
SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:Body
SOAP-ENV:Fault
faultcode
SOAP-ENV:Server
/faultcode
faultstring Procedure 'facility_id' not present
/faultstring
/SOAP-ENV:Fault
/SOAP-ENV:Body
/SOAP-ENV:Envelope

Is this what you were seeing? I noticed that the wsdl let me make calls without building request objects, but I didn't look very far into the code that wsdl2objc created

Hrishikesh Burkule said...

Good tutorial!
I am facing an issue. I followed all the steps to generate the source for a webservice named "eQSOASingletonServiceSvc" ( I checked the 'Add tag to service name')

Now when i try to use below line...
eQSOASingletonServiceSvBinding, i get a eQSOASingletonServiceSvBinding undeclared error while building.
I have something called eQSOASingletonPortBinding created in eQSOASingletonServiceSvc.h
I'm very new to iphone programming. Can you help?

Selim said...

Hi Brain
Thank you it is a great article.
But I have some problems too.
I have no LocalTimeSoap12Binding LocalTimeSoapBinding and didnt check tickbox.
Is any body can compile this sample , if yes can you send the sample code via email to me.

pilot007@gmail.com

best regards
Selim

brismith said...

Selim, did you remember to import LocalTime.h? (see step 4)

Anj said...

Thank you Brian, very good description, I was searching for this only, My work was done because of this description

occe said...

First, thanks for a great tutorial.

When I press "Parse WSDL" using http://www.ripedevelopment.com/webservices/LocalTime.asmx?wsdl to generate code it just says Finished! but no files are generated.

Gerson Freire de Amorim Filho said...

A great job! It works nice to me! God bless you, Brian!

keeffecanflyit said...

This is a brilliant tutorial, thank you.

But I need to do one thing, I need to add WSA addressing to the SOAP header in order for it to connect correctly.

The code needed to add is basically:

http://url.com/etc.svc

Could you by any chance let me know how to achieve this?

I've been trying to add it to the section where the SOAP message is created, EG: xmlNewNs(root, (const xmlChar*)"http://tempuri.org/", (const xmlChar*)"Service");

To no avail.

Any pointers appreciated.

Sincerely,
Cillian

brismith said...

Cillian, when I added xmlNewNs...tempuri.org...Service to serializedFormusingHeaderElements, my outbound xml includes xmlns:Service="http://tempuri.org" Is that what you're looking for? I would've expected the wsdl to provide the namespace defs

lolocuas said...

Thank you dude, you have help me today to finally be able to obtain some information from a SOAP service

Have an excellent day :)

webmaster said...

I'm having a problem at the following line:

LocalTimeSoap12Response *resp = [binding LocalTimeByZipCodeUsingParameters:request];

I understand this is not the exact code in the example, but the compiler didn't like your code and this seemed to be the only code the compiler would like. I end up getting a 500 Internal Server Error after calling the line above. I did follow the wsdl2objc Usage Instructions as well. Here's the error I get:

TimeProject[1335:40b] ResponseError:
Error Domain=LocalTimeSoap12ResponseHTTP Code=500 "internal server error" UserInfo=0x4e03790 {NSLocalizedDescription=internal server error}
Terminating in response to SpringBoard's termination.

Any hints would be greatly appreciated.

Dr. Cox said...

Hi,

I found that wsdl2objc does not create all classes when web services are JAX-WS based.

In particular, the ..SoapBinding class is not created.

On the wsdl2objc wiki, someone wrote.

"It turned out that the issue was that the referenced xsd namespace files were not accessible. In my case I had to place them in the appropriate location on my local system before it would generate the code correctly."

Unfortunately this solution is useless, because it does not tell which is the "appropriate location" for the namespace files.

I would like to know if the author of this very good tutorial, or any reader, could help me.

Thanks in advance
Michele

gourav said...

HI i m getting error

ResponseError:
Error Domain=LocalTimeSoapBindingResponseHTTP Code=500 "internal server error" UserInfo=0x6dad150 {NSLocalizedDescription=internal server error}

gourav said...

Hi i m getting following error

ResponseError:
Error Domain=LocalTimeSoapBindingResponseHTTP Code=500 "internal server error" UserInfo=0x6dad150 {NSLocalizedDescription=internal server error}

umesh said...

I am getting following error....
i changed application/soap+xml
to text/xml
in my Project.

ResponseError:
Error Domain=BasicHttpBinding_IService1BindingResponseHTTP Code=400 "bad request" UserInfo=0x6e42e70 {NSLocalizedDescription=bad request}

ksiedzulek said...

hi, great tutorial! thx, but I still have problem how to configure Xcode 4.3.2, I set under:
Project>Build Settings>Linking>"Other Linker Flags" to "-lxml2", and under Project>Build Settings>Apple LLVM compiler 3.2 - Language> "Other C Flags" to "-I/usr/include/libxml2" and I have error: 'libxml/tree.h' file not found....could you help me?

ksiedzulek said...

ok, I have solved my problem, but what should I do if WSDL value has a '.' in name? for example: "user.id". Xcode translate it of course with error because of using . and id:/

brismith said...

Glad to hear you got the project to build. About the '.' in the WSDL, there are a few posts at StackOverflow about this: http://stackoverflow.com/questions/9459986/use-of-dot-notation-in-web-service-wsdl-causes-sudzc-compile-errors

Rachana said...

Hi brismith,

I am getting the following error when I click on Test Button in the localTime code and that is why the responseBodyParts remains NULL


App[4549:f803] ResponseError:
Error Domain=LocalTimeSoapResponseHTTP Code=500 "internal server error" UserInfo=0x6ab3a80 {NSLocalizedDescription=internal server error}

Can you please help?

kiran kumar said...

BusinessService.h:57:83: error: expected a type [1]
- (WSHttpBinding_IBusinessServiceBindingResponse *)ProceedtoEsignUsingParameters:((null))aParameters ;
^
fatal error: too many errors emitted, stopping now [-ferror-limit=]


I am getting this error wile compiling

from.NetToiOS said...

I am following your example and I have achieved the result successfully. The example here returns "<LocalTimeByZipCodeResult>8/7/2012 9:10:03 PM</LocalTimeByZipCodeResult>". But my service actually returns an array of objects. It looks like this:
<getAllCategoriesResult xmlns:a="http://schemas.datacontract.org/2004/07/DocSyncService" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:Category> <a:categoryId>4</a:categoryId> <a:categoryName>IT</a:categoryName>
</a:Category>
<a:Category> <a:categoryId>6</a:categoryId> <a:categoryName>CPG</a:categoryName>
</a:Category>
</getAllCategoriesResult>
I want to display the values in <a:categoryName>("IT" and "CPG" in this case) in a tableview. Can you pls help me achieve this?

@man said...

ResponseError:
Error Domain=NSURLErrorDomain Code=-1012 "The operation couldn’t be completed. (NSURLErrorDomain error -1012.)" UserInfo=0x6858800 {NSErrorFailingURLKey=http://www.ripedevelopment.com/webservices/LocalTime.asmx, NSErrorFailingURLStringKey=http://www.ripedevelopment.com/webservices/LocalTime.asmx}
2012-10-10 15:37:44.593 TestWebService2[7930:207] ResponseError:
Error Domain=Connection Authentication Code=0 "Authentication Error" UserInfo=0x6850ee0 {NSLocalizedDescription=Authentication Error}

@man said...

I press button, and I get this error...