TMB: Apple (macOs and iOS) client

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

View source for the content of this page.