The principles of the Text Message Bus bus are described in the main TMB article. Do read that article first.
Brief summary of components
common (library) | General-purpose library. |
platform (library) | Cross-platform library. |
drawing (library) | Create Bezier curves for circle, square and triangle. |
networking (library) | Socket-level networking based on CFStream. Reads and writes data. |
class Figure | UIView that draws the geometric figure. |
class FigureGroup | UIView with a Figure and six buttons. |
class SubscribeGroup | UIView with the twelve subscribe/unsubscribe buttons. |
class MainView | Top-level UIView with two FigureGroup:s and a SubscribeGroup. |
class MainViewController | UIViewController for the main UIView. Singleton. |
class MessageBusTester | Application delegate. Processes incoming messages. Singleton. |
class MessageBusClient | Message bus logic. Sends and receives messages. Singleton. |
Application instance | Created from NIB file |
Libraries drawing and network offload some source from Figure and MessageBusClient respectively.
The platform library provides declarations and methods that make the rest of the code platform-independent.
Introduction
This article describes the Text Message Bus client for both macOS and iOS.
All source files are common to both targets. The code uses a set of typedef:s and dual-implementation functions to bridge the gap. See common.h for details. In addition, there are some #ifdef:s in the code, for overridden methods like initWithNibName (macOS), and willAnimateRotationToInterfaceOrientation (iOS).
It would have been confusing to the reader if this article had used my platform-independent identifiers. So, where types or other identifiers differ between the two targets, this article uses the macOS names. But if you look at the source code, you will see e.g. CtView instead of NSView or UIView.
I plan to write a separate article on describing platform-independence.
First I wrote the iOS program, and used Interface Builder. Then I rewrote the program for macOS, again using Interface Builder. But managing two interfaces in IB became tedious, and I hadn’t even started on handling rotation and different display sizes. With time, I was also becoming less and less enchanted with IB. There was a lot of strange behaviour; restarting XCode was needed far to often. I was wasting too much valuable programming time fighting IB.
So finally I gave up on Interface Builder, and created the GUI in code instead. One advantage of that was that when I wrote my own layout management, I could make it adapt to any orientation and display size. In fact, the code that does the layout does not know which platform it runs on, and has no concept of orientation. If the available window is higher than it is wide, it lays out in one way; if not, it lays out in a different way.
The program is still initialized from a NIB file created by XCode and Interface builder. But only a few things get done this way:
- Create the application instance.
- Create the application delegate (class MessageBusTester).
- Create an empty main view (class MainView).
- Create the controller for the main view (class MainViewController).
The GUI created from the NIB file is almost empty. On iOS there is a top-level view that covers the whole screen, and a smaller main view as an only child view. On macOS, there are also two views (for consistency) but the outer view does not cover the whole screen, and the inner view has the same size as the outer view. The main view populates itself in [MainView awakeFromNib]
Code that is common to both platforms but invoked from platform-dependent routines have been factored out to their own routines. One example is the MessageBusTester start]. On macOS it is called from MessageBusTester's implementation of [NSApplicationDelegate didFinishLaunchingWithOptions] and on iOS it is called from MessageBusTester's [UIApplicationDelegate applicationDidFinishLaunching].
Several classes have a construct method, called either from an init routine or from awakeFromNib. Remember that a few objects — classes MainView, MainViewController and the application delegate MessageBusTester — are still contructed from a NIB. Having these construct methods will make it easier to completely drop NIB files and create all object i code, should I decide to.
Since Objective-C is effectively a superset of ANSI C, there is some pure C code. Some utility routines (think NSLog) are natural candidates. Also, when it comes to fast low-level character handling, nothing beats C, even though some of the code is rather hairy.
Portability-wise, networking was a pleasent surprise. The socket-level networking code, written using CFStream, was not completely trivial to write. But once it got going on macOS, it needed no changes on iOS. BSD-style sockets calls are also available on macOS and iOS, but they do not integrate into the event loop. To use them, a separate thread would have been needed, either explicitly or possibly using Operation Queues.
There is not a singe @property anywhere. I tried to find at least a single use for them, just so that the reader would not think that I do not know how to use them, but to no avail. Being a firm believer in encapsulation and truly abstract data types, there simply was nothing that could be converted to a property. Confusingly, there is talk of properties in the code. There are methods with names like set_property, but they relate to the properties (colour and shape) of the geometric figures. And these do not translate into @property:s, since the set methods take an NSString but the get methods return an NSColor and and int repsectively.
Singleton classes, like MainView, keep a pointer the_instance. It can be read using the singleton’s class method +instance.
On macOS, the program is more or less complete. But on iOS it is not, and I do not plan to make it so. The program does not handle all the life-cycle events that it should. Specifically, the program should close the network connection when it has been in the inactive in the background for a while, and it should reopen the connection when needed. Resource hogging is not permitted, and iOS will kill the application after a while.
Execution flow
Starting
in main() calls NSApplicationMain from NIB creates application instance, a NSApplication or UIApplication application delegate, ($MessageBusTester) top-level view ($MainView) top-level view's controller, ($MainViewController) does [MainView awakeFromNib] sets the_instance to self creates the whole GUI in code two instances of $FigureGroup one instances of $SubscribeGroup does [MessageBusTester applicationDidFinishLaunching] does [self start] sets the_instance to self create the single instance of $MessageBusClient by calling +instance sets the_instance to self calls [MessageBusClient start] calls network_start() creates client socket connects to server set network_read_callback() as read callback
Incoming message
data becomes available on the socket the framework calls network_read_callback() reads data from the socket calls [ MessageBusClient on_incoming_data ] build lines from incoming character for each complete line calls [ MessageBusTester on_incoming_message ] decodes message into group, key and value calls [ MainViewController set_property ] calls [ MainView set_property ] calls [ FigureGroup set_property ] calls [ Figure set_property key value ]
Sending a figure property
Example for the upper Figure, property colour and value red.
someone calls [ Figure send :key :value] with "colour" and "red" builds string "/upper/colour red" calls [ MessageBusClient send_peer_message ] calls [ MessageBusClient send_message ] prepends a colon appends a newline calls network_write() calls CFWriteStreamWrite()
Colour or shape button clicked
Example for the upper “Red” button.
user clicks the upper "Red" button framework calls [ Figure redClicked ] calls [ Figure send ] with "colour" and "red" ... as Sending a figure property ...
“Send all” button clicked
user clicks on the "Send all" button framework calls [ MainView send_all ] for each FigureGroup (upper and lower) calls [ FigureGroup send_all ] calls [ Figure send_all ] for each property (colour and shape) calls [ Figure send ] with property and property value ... as Sending a figure property ...
Subscription button clicked
Connections from buttons to methods are set up in Interface Builder.
These buttons send their own labels.
user clicks on a subscribe or unsubscribe button framework calls [ MainView send_button_verbatim ] extracts the buttons text calls [ MessageBusClient send_command_message ] calls [ MessageBusClient send_message ] append a newline calls network_write() calls CFWriteStreamWrite()
Summary of source code
The code is mostly limited to header files. Only the most important parts of the files are included.
common.h
Some general-purpose macros and a type definition:
# define VECTSIZE(x) ((sizeof (x)) / (sizeof ((x)[0]))) # define DYNAMIC_CAST(cls, obj) ( [ (obj) isKindOfClass :[cls class] ] ? (cls *)(obj) : nil) # define LOG(args...) do { custom_log(0, __LINE__, 0, args); } while (0) # define LOGF(args...) do { custom_log(0, __LINE__, __FUNCTION__, args); } while (0) # define IN() do { LOG( @"%s", __FUNCTION__); } while (0) # define IN1(prefix) do { LOG(@"%s%s", prefix, __FUNCTION__); } while (0) typedef const char cchar;
And some functions that do not fit anywhere else:
void custom_log ( ...argument list...); NSString *a2nss (cchar *ascii); //--- ASCII C string to NSString NSString *rect_to_string (CGRect rect); CGRect irect (CGRect *rect); //--- round x, y, width and height to nearest integer CGSize min_fitting_size(NSArray *views);
Main program
int main(int argc, const char *argv[]) { return NSApplicationMain(argc, argv); }
drawing.h
NSBezierPath *drawCircle (void); NSBezierPath *drawSquare (void); NSBezierPath *drawTriangle(void);
These functions return Bezier curves. Implementation of these functions go into a separate module to make class Figure smaller and easier to read and manage.
Figure
@interface Figure : CtView - (instancetype)init :(NSString *)figure_name; - (void )set_property :(NSString *)key :(NSString *)value; - (void )send_all; -- Called by [FigureGroup send_all] @end
The view that draws the actual figure.
For set_property the name is either colour or shape. The value is red, green or blue for colour, or circle, square or tringle for shape.Not that “property” does not refer to a property in the Objective-C sense.
FigureGroup
@interface FigureGroup : CtView - (instancetype)init :(NSString *)figure_name; - (void )set_property :(NSString *)key value:(NSString *)value; - (void )send_all; Called by [MainView send_all] - (void )update_geometry; Called by [MainView update_geometry] @end
A custom view containing a Figure and its six asociated buttons. There are two figure groups, “upper” and “lower”.
set_property is called by [MainView set_property]
MainViewController
@interface MainViewController : CtViewController + (MainViewController *)instance; - (void)set_property :(NSString *)group key:(NSString *)key value:(NSString *)value; @end
Method set_property is called by [MessageBusTester on_incoming_message]. It calls [FigureGroup set_property].
MainView
@interface MainView : CtView + (MainView *)instance; - (void)set_property :(NSString *)group key:(NSString *)key value:(NSString *)value; - (void)send_all; @end
Method set_property is called by [MainViewController set_property]. It calls [FigureGroup set_property].
MessageBusClient.h
@interface MessageBusClient : NSObject @property (strong, nonatomic) NSWindow *window; + (MessageBusClient *)instance; - (void) start; -- Called by applicationDidFinishLaunching - (void) stop; -- Called by applicationWillTerminate - (void) send_peer_message :(cchar *)msg; -- Called by Figure - (void) send_command_message :(cchar *)msg; -- Called by MainView - (void) on_incoming_data :(char *)msg; -- Called by network_read_callback() @end
Methods send_peer_message and send_command_message both append a newline and pass the line on to network_write().In addition, send_peer_message prepends a colon.
MessageBusTester
@interface MessageBusTester : NSObject+ (MessageBusTester *)instance; - (void) on_incoming_message: (char *)msg; -- Called by MessageBusClient @end
networking
@class MessageBusClient; void network_start(NSString *host, int port, MessageBusClient *client); void network_stop (void); <em>-- Called by MessageBusClient</em> long network_write(char *buf); <em>-- Called by MessageBusClient</em> static void network_read_callback(...); <em>-- Called by framework</em>
Implements socket-level networking using CFStream.When data is available, network_read_callback() calls [MessageBusClient on_incoming_data].The instance to call is passed as a parameter to network_start().
You can reach me by email at “lars dash 7 dot sdu dot se” or by telephone +46 705 189090