Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member

The contents of this tutorial were based on another blog post: „SAP OData SDK Android for absolute beginners – Part 1” written by peter.csontos, available on the following link: http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/11/11/sap-odata-sdk-androi...

My post will help you to learn about the following in the least advanced way possible:

     -SAP Mobile Platform SDK for iOS

     -How to use the OData API to create an online store

     -Using Cocoapods to install dependencies for your project

You can think of my tutorial as an environment kickstarter for the OData SDK on iOS. If you feel like learning about the use of OData SDK itself would be more appropriate, I highly recommend you to check out the posts of kenichi.unnai/contentwho has already written a series of posts on that topic, suitable for both beginners and more advanced. You can start with the one below:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/09/16/smp3-odata-sdk-for-i...

After you have finished following my instructions in this tutorial, you will end up with a project similar to a „Hello World” for OData on iOS, if there was such a thing. In order to start, you only need basic knowledge of programming in Objective-C or Swift, using Xcode, RESTful web services and the OData protocol. You also need an Xcode installed on your Mac. If I managed to keep you interested, let's start!

Step 1: Install SAP Mobile Platform SDK for iOS

In case you don't have an SMP SDK already installed on your computer, this link will give you what you need to know: https://help.sap.com/smpsdk_re . The download and install process may provide you enough time to continue with the next two steps.

Step 2: Create a new iOS project in Xcode

Fortunately, you can choose between Swift and Objective-C languages for your project, because this tutorial covers both. Since Swift language is the future and the number of developers actively using it is highly growing, so despite its early problems it should be practiced in new projects as much as possible. Of course, you may need to (or want to) write your code in Objective-C. As a template, I used a single-view application for my project, but you can choose another for yours as you prefer. In the next steps I will refer to your project location as $PROJECTLOCATION, which by default  contains the project file with extension .xcodeproj.

Step 3: Disable App Transport Security

When building iOS apps using iOS 9 SDK, App Transport Security is enabled by default. ATS uses the TLSv1.2 protocol which converts all HTTP requests to HTTPS. Any attempts to connect to a server which does not support HTTPS protocol, or only supports TLS protocol older than v1.2 will fail. Since the reference services located at odata.org (and perhaps the service you want to use) only support HTTP, you have to disable ATS.

In the $PROJECTLOCATION/$PROJECTNAME folder an Info.plist file was automatically generated. Open it and add a new dictionary called "App Transport Security Settings" and create a new entry with key "Allow Arbitrary Loads" and value "YES". Here's a screenshot of how it should look:

Please note: disabling ATS is not recommended in most cases, so if you only connect to servers capable of using HTTPS protocol, leave it enabled.

Step 4: Setup project dependencies

I used Cocoapods to configure dependencies, but in case you don't want to use Cocoapods or something didn't work out with my steps, you can find another and more detailed tutorial here: http://help.sap.com/saphelp_smp3010sdk/helpdata/en/49/cc45e254f4492393801fc17391602f/content.htm

Here is how to do it with Cocoapods:

  -Close your project.

  -If not present, install Cocoapods by using the "gem install cocoapods" command in Terminal. Use "sudo" if needed and also note: some firewalls won't let gem to download. If you encounter any problems, connect to another network and try again.

  -In your terminal navigate to the $PROJECTLOCATION directory (or the location of your project file with extension .xcodeproj if not present by your settings) and use command "pod init". This will create a Podfile which you could have created yourself, but this way it will contain a template instead of being empty.

  -Open that Podfile in a text editor and you should create a content equivalent to the snippet below, or you can just copy-paste it. Note that $SMPSDKLOCATION is the path to your installation folder of the SAP Mobile Platform SDK and $PROJECTNAME is your project name which you should overwrite according to your environment.

##################################

platform :ios, "8.0"

target "$PROJECTNAME" do

inhibit_all_warnings!

pod 'NativeSDK/ODataOnline', :path => "$SMPSDKLOCATION/SAP/MobileSDK3/NativeSDK"

pod 'NativeSDK/ODataAPI', :path => "$SMPSDKLOCATION/SAP/MobileSDK3/NativeSDK"

end

##################################

  -Save the Podfile, run "pod setup" from Terminal.

  -If it succeeded, run "pod install"

If you encountered network related problems while executing the commands above, connect to another network and try again. If both have succeeded, the new file with the .xcworkspace extension is now your working area instead of the .xcodeproj file.

Step 5: Write your code

Now we only need some code to try if the OData SDK is working as intended. Open the .xcworkspace you have just created with Cocoapods. I suggest you to create a new file for the tester class and use a button to allocate and execute the test. In my example I pasted the code into the view controller class, which should always be avoided, but it helps to simplify my code snippet here. So let's start!

A: The Swift way

The SMP SDK for iOS was written in Objective-C which means you have to create a bridging header to import the files you need. If you are unsure how to do so, you can follow the official Apple tutorial.

Now import these headers in your new bridging header:

#import "SODataRequestParamSingleDefault.h"

#import "SODataResponseSingleDefault.h"

#import "SOData.h"

#import "SODataEntitySetDefault.h"

#import "SODataEntityDefault.h"

#import "SODataPropertyDefault.h"

#import "SODataOnlineStore.h"

Open the Swift file you want your OData online store to run from. For me it was ViewController.swift, but you should create your own empty class. Now start with the following definition:

var store : SODataOnlineStore!

Thanks to this first line, we can make sure both the pods and the bridging header were set up correctly. Jump to the definition of SODataOnlineStore (command-click by default). If you only see a big question mark, something went wrong. If it works, you can follow it with an optional UITextView object referenced from your storyboard and a String as its buffer. We will use them to inform ourselves on the screen if we managed to open the online store and it also displays the first entity to confirm our success in a more satisfying way. Error messages will also appear here if there were any. However, you can use NSLog for this purpose if you want to, but an UITextView object is easy to implement while being more elegant, plus you can drop your NSError right into it. So here go my two lines:

@IBOutlet var logDisplay: UITextView!

var log : String = ""


Our first method which opens the OData online store, needs a HttpConversationManager object in order to manage data traffic via the internet, since OData transfers data via HTTP or HTTPS as you may already know. Thanks to this manager, we don't have to worry about the details of these protocols after initializing it. Authentication is also not needed, since we will use a public reference service provided by odata.org. If you need to add authentication for services with such requirement,I suggest you to check out Kenichi Unnai's tutorials mentioned at the beginning of this post. The initialization of HttpConversationManager is as simple as this:

func openStore() {

     let manager = HttpConversationManager()


You need to specify the URL where the service is located. We will use one of the OData V4 reference services located at odata.org, but you will find other available services there, if you need to test your program with different, more advanced OData properties.

let url = NSURL(string: "http://services.odata.org/V4/OData/(S(readwrite))/OData.svc/")


Now we can initialize the online store with all its settings:

store = SODataOnlineStore(URL: url, httpConversationManager: manager)

The online store is initialized, it is ready to be opened. We will invoke the completion block version of the openStore method:

store.openStore { (store, error) -> Void in


Now that it's finished and we are in the completion block, we can invoke the method containing our first request in it. But before doing so, I suggest we  check if there was any error, and inform the user about it:

    if(error == nil){

        self.testREAD()

    } else {

        self.log = "FAILED TO OPEN STORE. ERROR: " + String(error)

        self.logDisplay.text = self.log

    }

}

}

At this point our online store is open, if there was no issue with connecting to the OData service. Let's implement the new testREAD method invoked in the code snippet above with a READ request in it.

func testREAD(){

Before executing the READ request, we have two essential arguments to set: the type of the request (READ, CREATE, etc.)  and the path of the resource we want access to:

let requestParam = SODataRequestParamSingleDefault(mode: SODataRequestModeRead, resourcePath: "Suppliers")


Thanks to the convenient interface, we only needed this line above to perform necessary initialization before actually making the request:

store.scheduleRequest(requestParam, completion: { (requestExecution, error) -> Void in


As you noticed, we use a completion block again to confirm the success of our request. Now we expect not only an error, but also a response with a payload which may or may not contain an entity set. But if an error has occurred, we may not even have a response or a payload. We should be cautious about these cases and everything should be checked in the right order. Thanks to the Swift language, setting the entity set and checking it's only one line:

if(error == nil){

    let responseSingle = requestExecution.response as! SODataResponseSingle

    if let entitySet = responseSingle.payload as? SODataEntitySet{

If both if-s evaluated to true, everything must be in order. But we like to see some result, so let's display what our entity set contains! The "Address" property is complex type, which needs some extra work to display properly:

self.log += "\nREAD SUCCEEDED.\n\nFirst entity in entityset \"Suppliers\":"

let readEntity = entitySet.entities[0] as! SODataEntityDefault

var readProperty = readEntity.properties["ID"] as! SODataPropertyDefault

self.log += "\n" + String(readProperty.name) + ": " + String(readProperty.value)

readProperty = readEntity.properties["Name"] as! SODataPropertyDefault

self.log += "\n" + String(readProperty.name) + ": " + String(readProperty.value)

readProperty = readEntity.properties["Concurrency"] as! SODataPropertyDefault

self.log += "\n" + String(readProperty.name) + ": " + String(readProperty.value)

readProperty = readEntity.properties["Address"] as! SODataPropertyDefault

self.log += "\n" + String(readProperty.name) + ": {"

let complexProperty = readProperty.value as! NSDictionary

for ( key, innerProperty ) in complexProperty {

     if let propertyValue = (innerProperty as? SODataProperty)?.value {

          self.log += "\n\t" + String(key) + ": " + String(propertyValue)}

     }

self.log += "\n}"

}

The buffer is now ready to be sent to display, but remember that we still have else branches left to implement for both if-s: in the inner else payload is not an entity set, in the outer one error is not nil.

  } else {

                    self.log = "\nERROR IN READ: RESPONSE NOT AN ENTITY SET"

                }

        } else {

            self.log = "\n\nERROR IN READ: " + String(error)

        }

        self.logDisplay.text = self.log

    } )//Completion block ends here

}//testREAD ends here

Finally, make sure to invoke the openStore method to start your test. I created a button and sent an action:

@IBAction func buttonPressed() {

    self.openStore()

}

That's all we needed to do to test the minimal OData online store. To make copy-pasting easier for you, here are the imports, declarations and the two methods without the need of formatting:

For your bridging header:

#import "SODataRequestParamSingleDefault.h"

#import "SODataResponseSingleDefault.h"

#import "SOData.h"

#import "SODataEntitySetDefault.h"

#import "SODataEntityDefault.h"

#import "SODataPropertyDefault.h"

#import "SODataOnlineStore.h"

For your new class or view controller:

var store : SODataOnlineStore!

@IBOutlet var logDisplay: UITextView!

var log : String = ""

func openStore(){

    let manager = HttpConversationManager()

    let url = NSURL(string: "http://services.odata.org/V4/OData/(S(readwrite))/OData.svc/")

    store = SODataOnlineStore(URL: url, httpConversationManager: manager)

    store.openStore { (store, error) -> Void in

        if(error == nil){

            self.testREAD()

        } else {

            self.log = "FAILED TO OPEN STORE. ERROR: " + String(error)

            self.logDisplay.text = self.log

        }

    }

}

func testREAD() {

    let requestParam = SODataRequestParamSingleDefault(mode: SODataRequestModeRead, resourcePath: "Suppliers")

    store.scheduleRequest(requestParam, completion: { (requestExecution, error) -> Void in

    if(error == nil) {

        let responseSingle = requestExecution.response as! SODataResponseSingle

        if let entitySet = responseSingle.payload as? SODataEntitySet {

            self.log += "\nREAD SUCCEEDED.\n\nFirst entity in entityset \"Suppliers\":"

            let readEntity = entitySet.entities[0] as! SODataEntityDefault

            var readProperty = readEntity.properties["ID"] as! SODataPropertyDefault

            self.log += "\n" + String(readProperty.name) + ": " + String(readProperty.value)

            readProperty = readEntity.properties["Name"] as! SODataPropertyDefault

            self.log += "\n" + String(readProperty.name) + ": " + String(readProperty.value)

            readProperty = readEntity.properties["Concurrency"] as! SODataPropertyDefault

            self.log += "\n" + String(readProperty.name) + ": " + String(readProperty.value)

            readProperty = readEntity.properties["Address"] as! SODataPropertyDefault

            self.log += "\n" + String(readProperty.name) + ": {"

            let complexProperty = readProperty.value as! NSDictionary

            for ( key, innerProperty ) in complexProperty {

                if let propertyValue = (innerProperty as? SODataProperty)?.value {

                    self.log += "\n\t" + String(key) + ": " + String(propertyValue)

                }

            }

            self.log += "\n}"

        } else {

            self.log = "\nERROR IN READ: RESPONSE NOT AN ENTITY SET"

        }

    } else {

        self.log = "\n\nERROR IN READ: " + String(error)

    }

        self.logDisplay.text = self.log

    } )//Completion block ends here

}//testREAD ends here

B: The Objective-C way

Open the Objective-C file you want your OData online store to run from (for me it was ViewController.m, but you should create your own empty class).

First you need to import several headers:

#import "SODataRequestParamSingleDefault.h"

#import "SODataResponseSingleDefault.h"

#import "SOData.h"

#import "SODataEntitySetDefault.h"

#import "SODataEntityDefault.h"

#import "SODataPropertyDefault.h"

#import "SODataOnlineStore.h"

We will also need the following properties:

@property (nonatomic, strong) SODataOnlineStore* store; //Online store as an object

@property (nonatomic, strong) id<SODataEntitySet> entitySet; //We will check the data we acquired by the READ request

@property (nonatomic, strong) NSMutableString *log; //We will append log messages to this object

@property (nonatomic, strong) IBOutlet UITextView *logDisplay; //Display the log (occasional refresh needed)

The UITextView object above is optional: we use it to inform us on the screen if we managed to open the online store. It also displays the first entity to confirm the success in a more satisfying way. Error messages will also appear here if there were any. However, you can use NSLog for this purpose if you want to, but an UITextView object is easy to implement while being more elegant, plus you can drop your NSError right into it. I also @synthesize-d all properties for a more pleasant code.

With the headers imported and the declarations in place, let's start implementing the first method which opens the OData online store:

- (void)openStore{

We need to init our log, because the first place to be accessed from is uncertain.

log = [[NSMutableString alloc] init];


Now we will use HttpConversationManager in order to manage data traffic via the internet, since OData transfers data via HTTP or HTTPS as you may already know. Thanks to this manager, we don't have to worry about the details of these protocols after initializing it. Authentication is also not needed, since we will use a public reference service provided by odata.org. If you need to add authentication for services with requirement such this, I suggest you to check out Kenichi Unnai's tutorials mentioned at the beginning of this post. The initialization of HttpConversationManager is simple as this:

HttpConversationManager* manager = [[HttpConversationManager alloc] init];


You need to specify the URL where the service is located. We will use the V4 reference service located at odata.org, but you can find several other reference services there, if you need to test your program with different, more advanced OData properties.

NSURL* url = [NSURL URLWithString:@"http://services.odata.org/V4/OData/(S(readwrite))/OData.svc/"];


Now we can initialize the online store with all its settings:

store = [[SODataOnlineStore alloc] initWithURL:url httpConversationManager:manager];


The online store is initialized, it is ready to open. We will invoke the completion block version of the openStore method:

[store openStore:^(id<SODataStoreAsync> store, NSError *error) {

Now that it's finished and we are in the completion block, we can invoke the method containing our first request in it. But before doing so I suggest we should check if there was any error and inform the user about it:

if(!error) {

  [self testREAD];

} else {

  [log appendString:[NSString stringWithFormat: @"\nFAILED TO OPEN STORE: %@",(id)error]];

  logDisplay.text = log;

}

}];

}

At this point our online store is open, if there was no issue with connecting to the OData service. Let's implement the testREAD method invoked in the code snippet above with a READ request in it:

- (void)testREAD {

Before executing the READ request, we have two essential arguments to set: the type of the request (READ, CREATE, etc.)  and the path of the resource we want access to:

SODataRequestParamSingleDefault *requestParam = [[SODataRequestParamSingleDefault alloc] initWithMode:SODataRequestModeRead resourcePath:@"Suppliers"];

Thanks to the convenient interface, we only needed this line above to initialize every essential data to actually make the request:

[store scheduleRequest:requestParam completion:^(id<SODataRequestExecution> requestExecution, NSError *error) {

As you noticed, we use a completion block again to confirm the success of our request. Now we expect not only an error, but also a response with a payload which may or may not contain an entity set. But if an error has occurred, we may not even have a response or a payload. We should be cautious about these cases and everything should be checked in the right order.

if(!error){

        id<SODataResponseSingle> responseSingle = (id<SODataResponseSingle>)requestExecution.response;

        if ([responseSingle.payload conformsToProtocol:@protocol(SODataEntitySet)]){

If both if-s evaluated to true, everything must be in order. But we like to see some result, so let's display what that entity set contains! The payload contains the entity set and we only want to see the first object in it. Here we can also inform ourselves about the success of the READ request, but for now we only need it in our buffer:

[log appendString:@"\nREAD SUCCEEDED.\nFirst entity in entity set \"Suppliers\":"];

entitySet = (id)responseSingle.payload;

SODataEntityDefault* readEntity = (SODataEntityDefault *)entitySet.entities[0];

Now the properties of our entity are ready to be displayed. The "Address" property is complex type, which needs some extra work to display properly:

SODataPropertyDefault* readProperty = (id)readEntity.properties[@"ID"];

[log appendString:[NSString stringWithFormat: @"\n%@: %@", readProperty.name, readProperty.value]];

readProperty = (id)readEntity.properties[@"Name"];

[log appendString:[NSString stringWithFormat: @"\n%@: %@", readProperty.name, readProperty.value]];

readProperty = (id)readEntity.properties[@"Concurrency"];

[log appendString:[NSString stringWithFormat: @"\n%@: %@", readProperty.name, readProperty.value]];

readProperty = (id)readEntity.properties[@"Address"];

[log appendString:[NSString stringWithFormat: @"\n%@: {", readProperty.name]];

NSDictionary *complexProperty = (id)[readProperty value];

[complexProperty enumerateKeysAndObjectsUsingBlock:^(id key, id innerProperty, BOOL *stop) {

       [log appendString:[NSString stringWithFormat: @"\n\t%@: %@", key, [[(id<SODataProperty>)innerProperty value] description]]];

}];

[log appendString:@"\n}"];

The buffer is now ready to be sent to display, but remember that we still have else branches left to implement for both if-s: in the inner else payload is not an entity set, in the outer one error is not nil.

        } else {

                [log appendString:@"\nERROR IN READ: RESPONSE NOT AN ENTITY SET"];

        }

} else {

            [log appendString:[NSString stringWithFormat: @"\nERROR IN READ: %@",(id)error]];

}

logDisplay.text = log;

}];//Completion block ends here

}//Method testREAD ends here

Finally, make sure to invoke the openStore method to start your test. I created a button and sent an action:

- (IBAction)buttonPressed{

    [self openStore];

}

That's all we needed to do to test the minimal OData online store. To make copy-pasting easier for you, here are the imports, declarations and the two methods:

#import "SODataRequestParamSingleDefault.h"

#import "SODataResponseSingleDefault.h"

#import "SOData.h"

#import "SODataEntitySetDefault.h"

#import "SODataEntityDefault.h"

#import "SODataPropertyDefault.h"

#import "SODataOnlineStore.h"

@property (nonatomic, strong) SODataOnlineStore* store; //Online store

@property (nonatomic, strong) id<SODataEntitySet> entitySet; //We will check the data we acquired with the response

@property (nonatomic, strong) NSMutableString *log; //We will append log messages to this object

@property (nonatomic, strong) IBOutlet UITextView *logDisplay; //Display the log (occasional refresh needed)

@synthesize store;

@synthesize entitySet;

@synthesize log;

@synthesize logDisplay;

- (void)openStore{

    log = [[NSMutableString alloc] init];

    HttpConversationManager* manager = [[HttpConversationManager alloc] init];

    NSURL* url = [NSURL URLWithString:@"http://services.odata.org/V4/OData/(S(readwrite))/OData.svc/"];

    store = [[SODataOnlineStore alloc] initWithURL:url httpConversationManager:manager];

    [store openStore:^(id<SODataStoreAsync> store, NSError *error) {

        if(!error) {

            [self testRead];

        } else {

            [log appendString:[NSString stringWithFormat: @"\nFAILED TO OPEN STORE: %@",(id)error]];

            logDisplay.text = log;

        }

    }];

}

- (void)testRead {

    SODataRequestParamSingleDefault *requestParam = [[SODataRequestParamSingleDefault alloc] initWithMode:SODataRequestModeRead resourcePath:@"Suppliers"];

    [store scheduleRequest:requestParam completion:^(id<SODataRequestExecution> requestExecution, NSError *error) {

        if(!error){

            id<SODataResponseSingle> responseSingle = (id<SODataResponseSingle>)requestExecution.response;

            if ([responseSingle.payload conformsToProtocol:@protocol(SODataEntitySet)]){

                entitySet = (id)responseSingle.payload;

                [log appendString:@"\nREAD SUCCEEDED.\nFirst entity in entity set \"Suppliers\":"];

                SODataEntityDefault* readEntity = (id)entitySet.entities[0];

                SODataPropertyDefault* readProperty = (id)readEntity.properties[@"ID"];

                [log appendString:[NSString stringWithFormat: @"\n%@: %@", readProperty.name, readProperty.value]];

                readProperty = (id)readEntity.properties[@"Name"];

                [log appendString:[NSString stringWithFormat: @"\n%@: %@", readProperty.name, readProperty.value]];

                readProperty = (id)readEntity.properties[@"Concurrency"];

                [log appendString:[NSString stringWithFormat: @"\n%@: %@", readProperty.name, readProperty.value]];

                readProperty = (id)readEntity.properties[@"Address"];

                [log appendString:[NSString stringWithFormat: @"\n%@: {", readProperty.name]];

                NSDictionary *complexProperty = (id)[readProperty value];

                [complexProperty enumerateKeysAndObjectsUsingBlock:^(id key, id innerProperty, BOOL *stop) {

                    [log appendString:[NSString stringWithFormat: @"\n\t%@: %@", key, [[(id<SODataProperty>)innerProperty value] description]]];

                }];

                [log appendString:@"\n}"];

            } else {

                [log appendString:@"\nERROR IN READ: RESPONSE NOT AN ENTITY SET"];

            }

        } else {

            [log appendString:[NSString stringWithFormat: @"\nERROR IN READ: %@",(id)error]];

        }

        logDisplay.text = log;

    }];//Completion block ends here

}//testREAD ends here

Step 6: Run your code

If you managed to build your project successfully and run it, you should see something like this:

If something didn't work, make sure you have connected to a network with your device or emulator and make sure the firewall on your network lets the service through. Either you have questions about this tutorial, or you have valuable additional information or corrections, feel free to share your thoughts in the comment section below. Thank you!

4 Comments