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]