SuperHTTP Chat (iOS, Cocoa)

This sample application provides a ROSuperHTTPClientChannel class usage example, the Cocoa implementation of the Super HTTP Channel. The superchannel is used for both synchronous and asynchronous requests requests and for receiving server callback events.

Server Requirements

The sample requires either the SuperHttp Chat sample (.NET) or the SuperHTTP Chat sample (Delphi) server.

Getting Started

Build the server using your preferred platform samples set and launch it somewhere in the local network. Build the sample project and launch it on the simulator or on the device. On the first view of the application you will see two text fields. The first contains the server target URL. You need to adjust it according to your server location inside the network (actually, you will need to change the IP address/hostname there). The second field is for the user's nickname in the chat. All users must have unique (case-insensitive) nicknames. When done changing the data, tap the Login button to proceed to the chat.
On the main chat screen you will see the chat history since the moment you logged in. You can enter some text to send to the chat in the text field at the upper part of the view, then tap the button with the arrow to send the message.

During the chat session you can tap the Who's here button to see the currently logged-in users list. Or tap the Logout button to stop your chat session. Pressing Home button on the device/simulator also leads to logout.

Examine the Code

All operations dealing with the remote services directly are incapsulated in the ServiceAccess class. It is implemented as a singleton created once and never deallocated during the application lifetime.
The chat server exposes two services: the LoginService (responsible for log in and log out) and the ChatService (responsible for the messaging itself). LoginService operations are considered synchronous and are executed via synchronous proxy classes:

LoginService_Proxy *loginProxy;
// ...
- (void)performLogin:(NSString *)nickName
{
    [loginProxy Login:nickName];
    loggedIn = YES;
}

- (void)performLogout
{
    if (loggedIn)
    {
        @try 
        {
            loggedIn = NO;
            [loginProxy Logout];
        }
        @catch (NSException * e) 
        {
            NSLog(@"Logout problem: %@", [e reason]);
        }
    }
}

Messaging operations are executed in an asynchronous manner:

ChatServerService_AsyncProxy *chatProxy;
// ...
- (void)sendMessage:(NSString *)aMessage to:(NSString *)aUser
{
    ROAsyncRequest *ar;
    if (aUser && ![aUser isEqualToString:@""])
        ar = [chatProxy beginTalkPrivate:aUser :aMessage start:NO];
    else
        ar = [chatProxy beginTalk:aMessage start:NO];
    ar.delegate = self;
    [ar start];
}

The service provides no feedback via the operation result, making processing asynchronous request a simple formality, except errors processing:

#pragma mark Async request delegate

- (void)asyncRequestDidComplete:(ROAsyncRequest *)request
{
    if ([request.messageName isEqualToString:@"Ping"]) 
    {
        [chatProxy endPing:request];
        return;
    }
    // And so on...
}

- (void)asyncRequest:(ROAsyncRequest *)request didFailWithException:(NSException *)exception
{
    if ([request.messageName isEqualToString:@"Ping"]) 
    {
        NSLog(@"Ping problem: %@", [exception reason]);
        return;
    }
    if ([request.messageName hasPrefix:@"Talk"]) 
    {
        [self exceptionThrown:[NSException exceptionWithName:@"AppException" 
                                  reason:[NSString stringWithFormat:@"Unable to send message: %@", [exception reason]] 
                                userInfo:nil]];
        return;
    }
}

A couple of words about the ping call (pingUntilLogout method). It is necessary to avoid the session expiration when there is no messaging for a long time. Thу ping operation is executed periodically on the background thread using the NSOperationQueue class (the piece of code below is from the LoginViewController class):

NSOperationQueue *opPool;
// ...
opPool = [[NSOperationQueue alloc] init];
// ...
NSInvocationOperation *pingOp = [[[NSInvocationOperation alloc] initWithTarget:[ServiceAccess instance] 
                                      selector:@selector(pingUntilLogout) 
                                    object:nil] autorelease];
[opPool addOperation:pingOp];

All messaging feedback from the server is sent using server callback events. They are received with the ROEventReceiver instance, which calls the corresponding delegate method when the event has been received:

// Configuring the event receiver
eventReceiver.channel = channel;
eventReceiver.message = message;
[eventReceiver registerEventHandler:self];
eventReceiver.active = YES;
// ...
// Event handling methods:
#pragma mark IChatEvents

- (void)Message:(NSString*)From :(NSString*)Target :(NSString*)Message
{
// ...
}

- (void)UserLogin:(NSString*)Nickname
{
// ...
}

- (void)UserLogout:(NSString*)Nickname
{
// ...
}

Event notifications are redirected to the ServiceAccess class delegate objects. This applications uses a kind of multicast delegate emulation; the delegates mutable array allowing multiple objects (view controllers) to react on events simultaneously.
The chat messages history is stored in the mutable array as a set of instances of the ChatMessageRecord class:

@interface ChatMessageRecord : NSObject
{
    NSDate *date;
    NSString *fromUser;
    NSString *messageText;
}

@property(readonly) NSDate *date;
@property(readonly) NSString *fromUser;
@property(readonly) NSString *messageText;

+ (ChatMessageRecord *)recordWithDate:(NSDate *)aDate user:(NSString *)aUser text:(NSString *)aText;
- (id)initWithDate:(NSDate *)aDate user:(NSString *)aUser text:(NSString *)aText;

@end

The ChatViewController uses the custom UITableViewCell to display this data. The logged-in users list is more simple, it is just an array of strings and there is no need to create a custom cell to display it.
Notice that the chat history is displayed in the reverse chronological order: the most recent message appears at the top of the table while the new message history records are appended to the end of the array. So we need to convert the visible message index into its real one (ChatViewController class):

int idx = ([[[ServiceAccess instance] chatHistory] count] - 1) - [indexPath row];
ChatMessageRecord *rec = [[[ServiceAccess instance] chatHistory] objectAtIndex:idx];

The chat history storage has a message limit controlled and maintained with the following two macros:

#define _CHAT_HISTORY_LENGTH    512
#define _CHECK_HISTORY_LENGTH   if ([chatHistory count] >= _CHAT_HISTORY_LENGTH) [chatHistory removeObjectAtIndex:0]