LiveChat for Mac & iOS behind the scenes
The purpose of this article is to present several technical design concepts implemented and proven to work well for over one year, since the first release of the Mac version of LiveChat operator application.
Back in 2010 when I started implementing the Mac version, the main task was to reach the feature level of the Windows version (developed for several years) as fast as possible, and also to prepare ground for an upcoming iOS version (available now in App Store). At that time I neither had access to the Windows version’s source code, nor was going to reuse any of it. Therefore I have settled the following constraints that yielded the following results (described in detail in following sections):
1. Source code must be divided into a framework part and GUI part
Where the framework part should contain most of the application logic and use API available on both Mac & iOS platforms, while the GUI part should be reduced to a minimum. Altogether it should match Model-View-Controller architecture.
Protocol.framework is now 60% of both platforms total source code, manages server communication and models such as
LCVisitor
orLCChat
.Protocol.framework does not use any platform specific API and relies only on Foundation.framework.
Mac & iOS use separate source code for views & controllers driving GUI, however these contain almost no application logic.
2. Source code should be self-describing and object-oriented
Utilizing (where possible) dynamic facilities of Objective-C.
All model classes visible to controllers are Objective-C classes, controllers have no direct access to protocol.
Application logic talks with the server through an OO proxy object using dynamic message dispatch to translate messages back and forth into text protocol commands. This makes the protocol layer really small in comparison to the whole application logic.
Protocol.framework
Protocol.framework contains most of the application logic, manages creating and
releasing model objects of LCVisitor
, LCOperator
& LCChat
classes, and
supplies GUI controllers with them. It contains LCServer private class which is
responsible for “message to protocol command mapping”. Applications’
controllers are delegates of LCAccount
class.
Mac version may dynamically follow model object’s state updates via Cocoa bindings (not available for iOS) without a need to manually process delegate methods. Therefore, for example inspector pane uses only bindings to update its contents, without actually knowing that it is part of complicated program logic. Inspector view contains just a layout and names for model properties presented to the user.
Strengths:
Putting whole application logic into Protocol.framework makes platform dependent GUI code reduced to minimum and responsible only for seamless translation of logical objects into GUI views.
Protocol.framework relies only on Foundation.framework that is a OO bridge for CoreFoundation and CFNetwork C libraries.
These libraries were published open-source and are known to build on several other platforms than OSX & iOS. Therefore in theory this code is portable to other platforms as well. Unfortunately source code of CFNetwork is no longer updated. Last available version matches OSX 10.4.11 Tiger.
Moreover CoreFoundation on other platforms is stripped from some functionality (named as CFLite) and latest releases have serious build problem on non-OSX platforms. There are several efforts to bring OSX API to other platforms, i.e.: OpenCFLite & PureFoundation fixing and extending CF & CFNetwork sources, Cocotron redoing whole Cocoa API.
Weaknesses:
Application must present same logic on all platforms. It is hard to code any exceptions.
Reusing Platform.framework to other platforms is possible in theory but problematic in practice. If all LiveChat applications were to use single protocol framework, C++ would be a better choice.
Server OO Proxy
While LCAccount
translates messages received into model objects, LCServer
class is responsible for translating textual protocol commands to Objective-C
messages sent and received by LCAccount
. LCServer
class implementation is
really small in comparison to other classes.
Once LCAccount
wants to initiate server communication using LCServer
, it
needs first to provide protocol command to selector maps: (1) incoming map
mapping to LCAccount
methods, and (2) outgoing map mapping to
LCOutgoingMethods protocol. LCServer
is the lowest level class that works
directly with sockets. Without going into details LiveChat protocol resembles
MSN and thanks to its linear (non-structural) construction it is possible to
implement such mapping.
Command Reception
Given that following incoming map is provided:
static LCCommandMapping incomingMap[] = {
// …
{ @"R0004", @selector(didReceiveFromClientWithIdentifier:message:senderNick:conferenceIdentifier:hidden:)},
// …
{ nil }
};
Upon each command reception LCServer
, parses NSMethodSignature
of mapped
method and marshals all incoming command arguments into method arguments doing
NSString
to argument type conversion.
It supports all basic types such as BOOL
, NSInteger
and NSString
. It also
supports NSArrays
for more advanced callback based commands (beyond the scope
of this article). Altogether, calls when receiving following message:
R0004|12341|Hello!|Joe|2134|0<LF>
[account didReceiveFromClientWithIdentifier:12341
message:@"Hello!"
senderNick:@"Joe"
conferenceIdentifier:2134
hidden:NO];
Where called method has the following signature:
- (void)didReceiveFromClientWithIdentifier:(NSInteger)clientIdentifier
message:(NSString *)message
senderNick:(NSString *)senderNick
conferenceIdentifier:(NSInteger)conferenceIdentifier
hidden:(BOOL)hidden
Of course this method is not called directly (like presented above), but in
fact LCServer
:
Constructs proper NSInvocation object via
[NSInvocation invocationWithMethodSignature:methodSignature]
Does marshaling and type conversion of received string arguments via
[methodSignature getArgumentTypeAtIndex:index]
example forNSUInteger
argument:if (!strcmp(type, "Q")) { NSUInteger value = [string integerValue]; [invocation setArgument:&value atIndex:index + 2]; }
finally calls
[invocation invoke]
.
LCServer
also handles situations when there are too many or too few arguments
received from the server:
In case there are too few, we may be working with an older server and all missing method arguments get nil, numeric get 0.
When there are too many arguments, then we are working with a server more recent than the application. In such case the application ignores extra arguments and emits a warning in the log.
Command Issue
Sending protocol commands to server is done in an opposite way. This time
LCServer
acts as OO proxy to LCAccount
that provides methods mapped to
protocol command and handled via Objective-C message forwarding mechanism
implemented by:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
struct objc_method_description mdesc =
protocol_getMethodDescription(outgoingProtocol, selector, YES, YES);
if (mdesc.types == NULL) {
return nil;
}
return [NSMethodSignature signatureWithObjCTypes:mdesc.types];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
NSString *commandName =
(NSString *)CFDictionaryGetValue(outgoingCommandMap,
(const void *)invocation.selector);
// … converts arguments to string representation and send command over socket
}
Once LCAccount
provides LCServer
an outgoing map and proxy interface:
static LCCommandMapping outgoingMap[] = {
// …
{ @"S0004", @selector(sendFromIdentifier:toChatWithIdentifier:message:) },
// …
{ nil }
};
@protocol LCOutgoingCommands
// …
- (void)sendFromIdentifier:(NSInteger)senderIdentifier
toChatWithIdentifier:(NSInteger)chatIdentifier
message:(NSString *)message;
// …
@end
It can cast internally LCServer
to id<LCOutgoingCommands>
and send message
to LCServer
:
[server sendFromIdentifier:431
toChatWithIdentifier:5488
message:@"Hello to you too!"];
Strengths:
Simplicity: No need to maintain long switch & case statements.
Separation: Only simple LCServer private class has direct access to the protocol.
Argument type casting done automatically at method invocation level.
Easy to debug: If program crashes we can deduce command sent by the server from stack backtrace.
Weaknesses:
- All protocol commands must follow the same rules for formatting and escaping arguments. It is really problematic to handle any exceptions in this model.
Protocol commands themselves must be linear (non-structural). Some recently added LiveChat protocol commands have structural arguments that must be parsed separately via external parsers, like XML parser for add-on action commands.
Conclusion
I believe it would not be possible to easily implement solutions described in this article with other API and languages such as C++ or Java. Unfortunately I observe that Objective-C dynamic facilities are less and less utilized by recent Mac OS X (Cocoa) and iOS (Mobile Cocoa) releases and newcomer developers. For example iOS (Mobile Cocoa), which is kind of a rewrite of OSX Cocoa does not implement bindings, like if they were considered deprecated.
Altogether sparse documentation for Objective-C principles, lack of solid examples (for bindings) and finally company politics that intentionally turn this powerful, dynamic language & its APIs into close environment supporting selected products only, deny original ideas of keeping NeXTStep and Objective-C solutions for modern applications running on all platforms.