The Objective-Cloud client library gives a client application easy and native access to a cloud application including support for concurrency and error handling.
This article is a conceptional summary of the client framework. It is not intended to be a getting started guide.
Before you start with this article you should have read:
This article contains three main chapters:
For a more API related documentation you should refer to the class references:
OCCService
OCCInvocation
OCCInvocationGroup
OCCInvocationContext
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:
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.
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
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.
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:
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:
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.
By default the client framework runs in the local mode. This has two consequences:
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.
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
).
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];
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:
Beside obvious differences in how the message is sent and if it blocks or not, the error handling is done differently.
Synchronous messaging is as easy as it can be: You use the usual Objective-C syntax. Errors are handled through the return value.
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];
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
}
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.
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.
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.
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];
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:
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:
-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:
nil
.Therefore you can use your original piece of code and put it simply in an invocation group.
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.