Objective-C in the Cloud

Client Libraries Conceptional Guide

The Objective-Cloud client library gives a client application easy and native access to a cloud application including support for concurrency and error handling.

About this Article

This article is a conceptional summary of the client framework. It is not intended to be a getting started guide.

Before you continue

Before you start with this article you should have read:

Overview

This article contains three main chapters:

  • Service Linking: Setting-up a service related connection between client application and cloud application.
  • Invocation: Sending messages synchronously and asynchronously
  • Data exchange: Arguments, return values and custom Types

Reference

For a more API related documentation you should refer to the class references:

  • OCCService
  • OCCInvocation
  • OCCInvocationGroup
  • OCCInvocationContext

Service Linking

Before you can use a remote service in a cloud application you have to connect a local class to the public class of the cloud application. This is done in three steps:

  • The cloud interface is used to describe the set of messages the service offers.
  • The client application uses a stand-in class, which imports the cloud interface.
  • This stand-in class is connected to the remote service.

Cloud Interface

Every class of a cloud application that can receive invocations has an Objective-C protocol called cloud interface. This protocol defines the public API of that class. Objective-Cloud uses that protocol to filter incoming messages as described in the documentation about invocation based applications.

On the client side this protocol is used to inform Xcode and clang about valid messages. In order to gain access to the protocol it should be imported into the client application project by creating a reference to it.

Stand-in Class

The client application sends it's messages to a stand-in class, which is a part of the client application. This class is derived from OCCService.

OCCService implements the messaging infrastructure together with OCCInvoation. Strictly speaking, you do not have to create a subclass of OCCService. Doing this is optional but highly recommended.

#import <OCClient/OCClient.h>

@interface MyService : OCCService
@end

The name of the class is used for addressing the remote service. Therefore it has to be identical to the remote service.

The stand-in class announces the availability of the methods declared by the cloud interface. This is done by adding a category, which imports the cloud interface as a protocol:

@interface MyService(CloudInterface)<MyServiceCloudInterface>
@end

The protocol is added to the category instead of the class. The reason for this is to avoid compiler warnings. However if you add a method declared in the cloud interface to the implementation of the stand-in class, it will be executed locally.

You can add properties or methods that are not declared in the protocol to the stand-in class. There is nothing special about it.

Of course it is possible to put both interfaces into one header file.

#import <OCClient/OCClient.h>

@interface MyService : OCCService
@end

@interface MyService(CloudInterface)<MyServiceCloudInterface>
@end

Linking

Before you send messages from the client application to the cloud application, the local stand-in class has to be linked to the remote service's class.

Local stand-in Class

The stand-in class is linked to the remote service class at runtime. Usually this is done at the very beginning of the client application, i. e. in +applicationDidFinishLaunching:. OCCService offers a set of methods to do so. The most convenient way is to use +linkToCloudApp:protocol:handler::

[MyService linkToCloudApp:@"MyCloudApp" protocol:@protocol(MyServiceCloudInterface) handler:
^(__unsafe_unretained Class serviceClass, NSError *error)
{
  // Add code to be executed after linking
}];

There is another method +linkToCloudApp:protocol:URLSession:handler: that you can use to pass a custom NSURLSession. A custom NSURLSession allows you to configure certain HTTP related parameters.

The name of the remote service class is taken from the receiver of this message, in the above example MyService.

After connecting to the cloud application's service class the handler is called on the main thread. If it failed, serviceClass is Nil and error describes the reasons why the linking failed.

You can send this message in the main thread, because it will return fast. Additionally the current implementation does not contact the cloud application for two reasons:

  • One might think that this is the right place to check, whether the cloud interface the client app uses fits to the cloud interface the cloud app published. But a mismatch there is a programming error that does not have to be solved at this place.
  • It is a misbelief that you can check for the existence of a connection at the beginning of your app when the classes are linked. Even if you checked the reachability of a service in one moment in the very next moment it can be gone. You have to check the result of every message, if you are interested in that.

Remote Host

By default the stand-in class is linked to a cloud application's counterpart running behind a local instance of Terrasphere. The local instance of Terrasphere is assumed to be reachable at localhost:10000.

You can change that default behavior. There are two modes:

  • local mode
  • remote mode

Both modes let you set the domain/host of Terrasphere. This allows you to easily change from testing locally to testing remotely and gives you the ability to also test on an iOS device by specifying the host of your development machine which is running Terrasphere.

Local and remote Mode

By default the client framework runs in the local mode. This has two consequences:

  • HTTP instead of HTTPS is used.
  • The domain does not contain a subdomain for the cloud application name.

You can switch to the remote mode (and back) by using +setUsesRemoteHost: (OCCService). You have to do this before linking the stand-in class to the remote class.

[OCCService setUsesRemoteHost:YES];
// and then you can link

When you use the client on localhost then the client framework will include the name of the cloud application in the host header field as a subdomain.

Host Domain

In the local mode the Terrasphere's domain defaults to localhost:10000. You can change with +setLocalHost: (OCCService).

The remote domain defaults to obcl.io. It can be set with +setRemoteHost: (OCCService).

Usual Configuration

By setting the mode and the domains you can configure your host address for all configurations.

Desired Configuration Set … to …
Client App runs on Cloud App runs on Remote Mode Local Host Remote Host
Developer's Mac (OS X) Developer's Mac
Developer's Mac (iOS simulator) Developer's Mac
iOS Device Developer's Mac Developer's Mac
Anywhere Dedicated development Mac Dedicated Mac
Anywhere Objective-Cloud YES
Anywhere Self hosted Objective-Cloud YES Objective-Cloud Mac

You can get the hostname of Terrasphere by executing the following command in Terminal:

$ hostname
Amins-MacBook-Air.local

It is convenient to set local and remote host once, if necessary and to switch between development and production hosts by simply changing the argument passed to +setUsesRemoteHost: from NO to YES (and vice versa):

//  Configure hosts one time: 
//  Development on developer's Mac, simulator or iOS device, production self-hosted
[OCCService setLocalHost:@"Amins-MacBook-Air.local:10000"];
[OCCService setRemoteHost:@"mycompany-objective-cloud.com"];

//  Switch 
[OCCService setUsesRemoteHost:YES];

If you are working in a team with access to a repository you should not include the name your Mac as the local host in a source file that is managed by the repository. Otherwise the client applications searches randomly for the cloud application on different Macs in your local area network. This is certainly a good opportunity to confuse your co-workers which is funny once and only once.

Instead set-up a dedicated Mac running Terrasphere and the cloud application, if the cloud application is stable. Otherwise add a define into a separate header file, which is not included in the repository and use it in the message to link the stand-in class:

//  ExcludeThisHeaderFromRepos.h
NSString *myMacsHostName = @"Amins-MacBook-Air.local:10000";
#import "ExcludeThisHeaderFromRepos.h"

[OCCService setLocalHost:myMacsHostName];

Invocation

After linking a stand-in class to the cloud application's service it is ready to use. You can send messages to the service to invoke methods. This is primarily done with the usual Objective-C syntax.

The client framework offers different ways for method invocation:

  • Synchronous invocation: Send messages in a blocking manner.
  • Asynchronous invocation: Retrieve an invocation object and send messages in a non-blocking manner.
  • Invocation group: Send messages using the synchronous API in an asynchronous invocation group.

Beside obvious differences in how the message is sent and if it blocks or not, the error handling is done differently.

Synchronous Invocation

Synchronous messaging is as easy as it can be: You use the usual Objective-C syntax. Errors are handled through the return value.

Sending Messages

Using the usual Objective-C syntax for sending the message and retrieving the return value makes synchronous messaging very readable:

NSString *returnValue = [MyService doSomethingUseful];

You can add arguments to the message:

NSString *returnValue = [MyService doSomethingUsefulWith:@5];

Error Handling

If an error occurs, it is delivered as a return value. Therefore you have to check the return value before you use it. You can do this easily with -hasObjectiveCloudError, which is declared in a category of NSObject. So the signature of the method can have any return type, but needs a return type.

NSString *returnValue = [MyService doSomethingUseful];
if ([returnValue hasObjectiveCloudError])
{
  // Do error handling
}
else
{
  // Do some processing
}

Asynchronous Invocation

Using asynchronous invocations a message is a two-step task: First you retrieve an invocation object and then you send the message to that object.

Retrieving the Invocation Object

The invocation object is delivered by the stand-in class for the service. Typically you get it with -invokeWithHandler: passing a completion handler, which gets the return value:

id<MyServiceCloudInterface> invocation = [MyService invokeWithHandler:
^(BOOL success, id response)
{

}];

The parameters of the handler are explained in the subchapter Error Handling.

Sending the Message

The message is then sent to the invocation object as usual:

id<MyServiceCloudInterface> invocation = [MyService invokeWithHandler:^(BOOL success, id response){}];
[invocation doSomethingUseful];

You can add arguments to the message:

[invocation doSomethingUsefulWith:@5];

The execution is non-blocking. The return value of the above message is undefined. You should drop it.

Error handling

Using asynchronous invocation the return value is an argument of the completion handler. Beside this you get a boolean which signals an error.

id<MyServiceCloudInterface> invocation = [MyService invokeWithHandler:
^(BOOL success, id response)
{
  if (success) 
  {
    NSString *returnValue = repsonse;
    // do some processing
  }
  else
  {
    NSError *error = response;
    // Do error handling
    
  }
}
[invocation doSomethingUseful];

Invocation Group

Sending a bunch of messages to a cloud application leads to some awkwardness you do not have in a local environment executing a piece of code asynchronously. Let's have a simple example:

dispatch_queue_t cloudQueue = dispatch_queue_create( @"Cloud Queue", NULL);
dispatch_asynch( cloudQueue,
^()
{
  NSString *UUID = [PDFService storePDF:data];
  NSArray *hits = [PDFService findString:@"Objective-Cloud" inPDFWithUUID:UUID];
  if (hits!=nil)
  {
    // Propagate the hits to the UI.
  }
  else 
  {
    // Do some error handling
  }
}];

This is straight forward.

Using the non-blocking API you face several problems in this situation:

  • You probably want to use the result of one invocation as an argument of another invocation. Since the order of invocations using the asynchronous API is not defined, you have to nest the messages.
  • You have to check for an error every time.
id<PDFService> storeInvocation = [PDFService invokeWithHandler:
^(BOOL success, id response)
{
    if (success)
    {
      id<PDFService> findInvocation = [PDFService invokeWithHandler:
      ^(BOOL success, id response)
      {
        if (success) 
        {
          //  Propagate the hits to the UI.
        }
        else
        {
          // Do some error handling.
        }
      }];
      [findInvocation findString:@"Objective-Cloud" inPDFWithUUI:UUID];
    }
    else
    {
      // Do some error handling
    }
}];
[storeInvocation storePDF:data];

The code is less readable. The reasons are obvious:

  • A linear task became nested.
  • The order of the messages is reverse in the source code.
  • You have to check for errors every time.
  • Therefore error handling is distributed to two locations. (The original code could use the message to nil behavior of Objective-C, if -storePDF simply returns nil on an error.)

Using the synchronous API things become a little bit easier but are still inconvenient:

dispatch_queue_t cloudQueue = dispatch_queue_create( @"Cloud Queue", NULL);
dispatch_asynch( cloudQueue,
^()
{
  NSString *UUID = [PDFService storePDF:data];
  if ([UUID isObjectiveCloudError])
  {
    // Do some error handling.
  }
  else
  {  
    NSArray *hits = [PDFService findString:@"Objective-Cloud" inPDFWithUUID:UUID];
    if ([hits isObjectiveCloudError])
    {
      // Propagate the hits to the UI.
    }
    else 
    {
      // Do some error handling
    }
  }
});

As you can see, it is better readable, but still not as straight forward as the original piece of code. One might say that this is the nature of sending messages over an unsafe connection. But analyzing such situations we detected that most of the extra code is boiler plate code, identical each time. With invocation grouping things are more straight forward again:

[OCCInvocationGroup executeBlock:
^(OCCInvocationContext *context)
{
  NSString *UUID = [PDFService storePDF:data];
  NSArray *hits = [PDFService findString:@"Objective-Cloud" inPDFWithUUID:UUID];
  if ([context error]!=nil)
  {
    // Propagate the hits to the UI.
  }
  else 
  {
    // Do some error handling
  }  
}];

This code works without boiler plate code for some reasons:

  • Inside the block everything works synchronously. You do not have to nest something to keep it in the correct order.
  • If an method invocation fails, some optimizations are applied
    • The error is not reported via the return value. This becomes simply nil.
    • Consecutive messages are not transmitted.

Therefore you can use your original piece of code and put it simply in an invocation group.

Data Exchange

Sending messages includes transferring data to the cloud as arguments and back from the cloud as return values. As you could see in the above examples this is done as usual as it can be, especially with invocation groups.

You can use any type that is JSON serializable. To accomplish this at the server side, refer here. You can simply import such a definition to the client application to use it.