Chat Demo - iPhone Client
The Ice Touch distribution includes a chat client for use on an iPhone or iPod touch. Since Ice Touch includes a server-side run time, the chat client is able to use the push model and wait passively for the server to deliver chat updates via callbacks. The sample code presented below gives you a taste of what it's like to write an Ice application in Objective-C. If you'd like to see the complete source code, we encourage you to download the Ice Touch distribution.
Creating a Session
One of the first tasks of a push client is to establish a session with the Glacier2 router. The program presents the user with a login form that requests information such as the server host, user name, and password. Click on the thumbnail image to see the login form.
The client begins by creating a router proxy and configuring it as the default router for the communicator:
id<ICECommunicator> communicator = ... NSString* s = @"Glacier2/router:..."; id<ICEObjectPrx> proxy = [communicator stringToProxy:s]; id<ICERouterPrx> router = [ICERouterPrx uncheckedCast:proxy]; [communicator setDefaultRouter:router];It is safe to perform these actions in the main thread because they do not block. However, the remaining steps in the login process may block the calling thread and prevent the user interface from being updated in a timely manner, therefore the client spawns a separate thread dedicated to completing this initialization. The thread continues with the creation of a Glacier2 session:
id<Glacier2RouterPrx> glacier2router = [Glacier2RouterPrx checkedCast:router];
id<Glacier2SessionPrx> glacier2session =
[glacier2router createSession:username password:password];
id<ChatChatSessionPrx> sess = [ChatChatSessionPrx uncheckedCast:glacier2session];
The client down-casts the router proxy to the derived interface Glacier2::Router
using a checked cast to verify that the target object supports the expected interface.
With the router's proxy in hand, the client can create a Glacier2 session.
The createSession operation returns a proxy of type
Glacier2::Session, and the client narrows this proxy to the derived interface
Chat::ChatSession that we saw earlier. The client narrows
the session proxy using an uncheckedCast because it already knows that the session
object implements this interface.
After successfully creating the session, the client must take care of a little house-keeping before it can instantiate its callback object. The first step is to create an object adapter, which is the Ice construct responsible for dispatching incoming requests to Ice objects. Activating the object adapter allows the Ice run time to begin dispatching requests:
id<ICEObjectAdapter> adapter =
[communicator createObjectAdapterWithRouter:@"ChatDemo.Client" router:router];
[adapter activate];
Every Ice object requires a unique identity, which is a structure consisting of two strings representing a name and a category. When using callbacks with Glacier2, the client must use a category supplied by Glacier2. The code below prepares the identity for the callback object:
ICEIdentity* callbackId =
[ICEIdentity identity:[ICEUtil generateUUID]
category:[router getCategoryForClient]];
Now the client can instantiate its callback object. Since the client activated the object adapter earlier, it is possible for requests to be dispatched to this object as soon as we add it to the object adapter. In turn, the object adapter returns a proxy that we can down-cast to the appropriate interface:
id<ICEObjectPrx> proxy =
[adapter add:[ChatRoomCallbackI
chatRoomCallbackWithTarget:self.chatViewController]
identity:callbackId];
id<ChatChatRoomCallbackPrx> callback =
[ChatChatRoomCallbackPrx uncheckedCast:proxy];
The last step is to join the chat room, which we accomplish by invoking setCallback on the session:
[sess setCallback:callback];
Asynchronous Invocations
When an interactive program needs to perform a series of related but potentially blocking actions, it is convenient to group them together and execute them in a separate thread, as we did in the previous section to create a session. However, this strategy would quickly grow tedious in situations where you only need to make one remote invocation. Fortunately, Ice guarantees that invoking a remote operation asynchronously will never block the calling thread, and the iPhone chat client uses this feature extensively. For example, the code below shows how the client invokes an asynchronous version of the send operation to publish a newly-typed chat message:
-(BOOL)textFieldShouldReturn:(UITextField*)theTextField
{
if(theTextField.text.length > 0) {
[session
send_async:[ICECallbackOnMainThread callbackOnMainThread:self]
response:nil
exception:@selector(exception:)
message:theTextField.text];
}
return YES;
}
The first argument to send_async is the AMI callback object, followed by selectors for methods of the callback object that will be invoked when the operation completes successfully or raises an exception. Here the client ignores a successful response by supplying nil as the value for the response argument, and specifies exception: as the callback if an error occurs.
The Ice run time invokes selectors on the callback object from outside the main thread, which can be a problem if a callback method needs to update the user interface. Normally the callback would consist of tedious code that arranges for the desired action to be performed in the main thread so that the user interface could be safely updated. Fortunately, Ice Touch offers a convenient alternative in the ICECallbackOnMainThread class. As its name implies, this class arranges for the callback methods to be invoked by the main thread. You can supply an instance of this class as the callback object for any asynchronous invocation; the initializer argument indicates the target object on which the selectors should be performed.
Eventually the message published via send_async will return to the client in the form of a chat update callback from the server. Our servant, an instance of ChatRoomCallbackI, declares its send method as follows:
@interface ChatRoomCallbackI : ChatChatRoomCallback
...
-(void) send:(ICELong)timestamp name:(NSMutableString *)name
message:(NSMutableString *)message current:(ICECurrent *)current;
@end
The implementation of send bundles its arguments and arranges for the main
thread to invoke a method of the same name on our controller object:
-(void) send:(ICELong)timestamp name:(NSMutableString *)name
message:(NSMutableString *)message current:(ICECurrent *)current
{
SEL selector = @selector(send:name:message:);
NSMethodSignature* sig =
[[target class] instanceMethodSignatureForSelector:selector];
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:target];
[invocation setSelector:selector];
[invocation setArgument:×tamp atIndex:2];
[invocation setArgument:&name atIndex:3];
[invocation setArgument:&message atIndex:4];
[invocation retainArguments];
[invocation performSelectorOnMainThread:@selector(invokeWithTarget:)
withObject:target waitUntilDone:NO];
}
Without the ICECallbackOnMainThread helper class, applications would need
to use a similar technique to handle AMI callbacks that must execute in the main thread.
In our controller, the implementation of send instantiates a ChatMessage object and invokes append:
-(void)send:(ICELong)timestamp name:(NSMutableString *)name
message:(NSMutableString *)message
{
[self append:[ChatMessage chatMessageWithText:message who:name
timestamp:timestamp]];
}
The image on the right shows the iPhone client's chat history. Click on the thumbnail to see the full image.
| Previous: Silverlight Clients |