Objective-C Example

The Objective-C language is most commonly associated with the iOS and MacOS-X platforms from Apple. The framework that is most commonly used on these platforms with the Objective-C language is known as Cocoa. This tutorial looks at how eTXT's messaging REST API can be used from the Objective-C language on the MacOS-X platform with Cocoa in order to deliver a message to be sent to a recipient. Similar code could also be used on the iOS or the GNUstep platforms. You will need to install the X-Code developer tools that are provided by Apple to follow these instructions. This tutorial uses plain Objective-C without making use of external libraries or more recent additions to the Objective-C language. In order to run this code, you will need to have a eTXT username and password.

This tutorial demonstrates the use of asynchronous HTTP connections to send a message through the eTXT server. The use of asynchronous HTTP connections is important because it allows for the iOS or MacOS-X user-interface and other parts of the application to keep functioning while the message is being sent over the network. The logic for performing this operation uses the URL connection classes from the Cocoa library. In this tutorial, a class BMSender will keep track of which messages are being sent on which HTTP connections and coordinate the request-response life-cycle of the HTTP requests. This tutorial only provides for a single message to be sent, but the design could support multiple messages being transmitted concurrently. A main.m file constitutes a test-harness that demonstrates the use of the BMSender class and provides ancillary structure and logic for running the demonstration.

Setup

Create a new X-Code project;

  • Run the X-Code development tool
  • File →New →New Project...
  • MacOS-X →Application →Command Line Tool
  • Provide settings for your project
    • Provide a name and company identifier for your project
    • Type : Foundation
    • Do not use automatic reference counting
  • Choose a location for your project

Automatic Reference Counting

Using automatic reference counting (ARC) with this tutorial should function just the same; this tutorial is only avoiding use of ARC to demonstrate functionality for plain Objective-C scenarios.

Write the code

Create a new Objective-C class called "BMSender". This can be achieved under File →New →New File... The BMSender class will prepare, send and coordinate the HTTP requests that are made to the server in order to send messages.

In your project you will find a BMSender.h file. Edit this file to contain the material below. This file defines the public interface for the BMSender class.

#import <Foundation/Foundation.h>

/*
 This protocol is implemented by the client in order to provide a
 feedback mechanism.  In this example, the feedback is simple and
 only conveys success and failure states by invoking one of two
 methods.  Your own delegate should implement these two methods.
 */
@protocol BMSenderDelegate <NSObject>
- (void) successfullySentMessageIdentifiedBy:(NSString *)messageId;
- (void) failedToSendMessageIdentifiedBy:(NSString *)messageId;
@end
/*
 This is the public interface for the sender class.  It can be
 instantiated with your username and password.  Once
 instantiated, this object is able to send messages using the
 'sendMessage' method.
 */
@interface BMSender : NSObject {
    NSString *_userId;
    NSString *_password;
    NSMutableArray *_connectionStatuses;
}
- (id) initWithUserId:(NSString *)userId password:(NSString *)password;
- (void) sendMessage:(NSString *)body
                  to:(NSString *)recipient
        identifiedBy:(NSString *)messageId
            delegate:(id <BMSenderDelegate>)delegate;
@end
Now open the corresponding "BMSender.m" file and edit it with the implementation shown below;

BMSender.m
#import "BMSender.h"

// #########################################################
#pragma mark Prelude and Constants

#define ENDPOINT_SENDMESSAGE @"https://www.etxtservice.co.nz/api/3/sms/out"
#define ERRCHECK_UNKNOWNCONNECTION NSAssert(nil!=conState,@"a managed connection should have been able to be found, but was not present - this should not be possible");
#define SHOULD_LOG 1

// #########################################################
#pragma mark Sender Connection State Declaration and Implementation

/*
 This class is private and should not need to be used.  Its
 purpose is to map the HTTP connections through to the
 in-flight message's message-id and the delegate to which
 feedback should be sent.  It also maintains state regarding
 the HTTP response that comes back.  This is purely a model
 object with no controller or logic.
 */

@interface BMSenderConnectionState : NSObject {
    NSString *_messageId;
    NSURLConnection *_connection;
    id <BMSenderDelegate> _delegate;
    NSInteger _statusCode;
}

- (id) initForMessageIdentifiedBy:(NSString *)messageId
                     onConnection:(NSURLConnection *)connection
                     withDelegate:(id <BMSenderDelegate>)delegate;

- (NSString *) messageId;
- (NSURLConnection *) connection;
- (id <BMSenderDelegate>) delegate;
- (void) setStatusCode:(NSInteger)value;
- (NSInteger) statusCode;

@end

@implementation BMSenderConnectionState

- (id) initForMessageIdentifiedBy:(NSString *)messageId
                     onConnection:(NSURLConnection *)connection
                     withDelegate:(id <BMSenderDelegate>)delegate {
    NSAssert(0!=[messageId length],@"the message id is required");
    NSAssert(nil!=connection,@"the connection is required");
    [super init];
    _messageId = [[NSString alloc] initWithString:messageId];
    _connection = [connection retain];
    _delegate = [delegate retain];
    _statusCode = -1;
    return self;
}

- (void) dealloc {
    [_messageId release];
    [_connection release];
    [_delegate release];
    [super dealloc];
}

- (NSString *) messageId { return _messageId; }
- (NSURLConnection *) connection { return _connection; }
- (id <BMSenderDelegate>) delegate { return _delegate; }
- (void) setStatusCode:(NSInteger)value { _statusCode = value; }
- (NSInteger) statusCode { return _statusCode; }
@end

// #########################################################
#pragma mark Sender Implementation

/*
 A category is used here to declare the private methods on
 this class.
 */

@interface BMSender (BMSenderPrivate)
- (BMSenderConnectionState *) senderConnectionInTransitForMessageIdentifiedBy:(NSURLConnection *)connection;
@end

@implementation BMSender

- (id) initWithUserId:(NSString *)userId password:(NSString *)password {
    NSAssert(0!=[userId length],@"the user id is required");
    NSAssert(0!=[password length],@"the password is required");
    [self init];
    _userId = [[NSString alloc] initWithString:userId];
    _password = [[NSString alloc] initWithString:password];
    _connectionStatuses = [[NSMutableArray alloc] init];
    return self;
}

- (void) dealloc {

    // stop any active connections

    for(BMSenderConnectionState *conState in _connectionStatuses) {
        [[conState connection] cancel];
    }

    [_userId release];
    [_password release];
    [_connectionStatuses release];

    [super dealloc];
}

- (void) sendMessage:(NSString *)body
                  to:(NSString *)recipient
        identifiedBy:(NSString *)messageId
            delegate:(id <BMSenderDelegate>)delegate {

    NSAssert(0!=[body length],@"the body must be supplied to send a message");
    NSAssert(0!=[recipient length],@"the recipient must be supplied to send a message");
    NSAssert(0!=[messageId length],@"the recipient must be supplied to send a message");

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:ENDPOINT_SENDMESSAGE]];
    NSString *payload = [NSString stringWithFormat:
                         @"userId=%@&password=%@&to=%@&body=%@&messageId=%@",
                         [_userId stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
                         [_password stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
                         [recipient stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
                         [body stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
                         [messageId stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];

    [request setCachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData];
    [request setHTTPMethod:@"POST"];
    [request addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:[payload dataUsingEncoding:NSUTF8StringEncoding]];

    // the conection is made to the REST server in order to
    // send the message.  A connection state is then stored in the
    // sender so it can manage the connection as it goes through the
    // HTTP request-response lifecycle.

    NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
    BMSenderConnectionState *conState = [[BMSenderConnectionState alloc] initForMessageIdentifiedBy:messageId
                                                                                                                onConnection:connection withDelegate:delegate];
    [_connectionStatuses addObject:conState];
    [conState release];

    [request release];

#ifdef SHOULD_LOG
    NSLog(@"s; sending message <<%@>> to %@",messageId,recipient);
#endif
}

/*
 This is an internal method that looks up the connection state for
 a given HTTP connection.
 */

- (BMSenderConnectionState *) senderConnectionInTransitForMessageIdentifiedBy:(NSURLConnection *)connection {
    for(BMSenderConnectionState *conState in _connectionStatuses) {
        if([conState connection] == connection)
            return conState;
    }

    return nil;
}

// implementation of method from NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection
  didFailWithError:(NSError *)error {
    BMSenderConnectionState *conState = [self senderConnectionInTransitForMessageIdentifiedBy:connection];
    ERRCHECK_UNKNOWNCONNECTION
    [[conState delegate] failedToSendMessageIdentifiedBy:[conState messageId]];
    [_connectionStatuses removeObject:conState];

#ifdef SHOULD_LOG
    NSLog(@"s; connection failed for <<%@>>",[conState messageId]);
#endif
}

// implementation of method from NSURLConnectionDelegate
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
                  willCacheResponse:(NSCachedURLResponse *)cachedResponse {
    return cachedResponse;
}

// implementation of method from NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response {
    BMSenderConnectionState *conState = [self senderConnectionInTransitForMessageIdentifiedBy:connection];
    ERRCHECK_UNKNOWNCONNECTION

    // store the HTTP status code away in the connection state until
    // the request-response cycle is completed and then it will know
    // if the message went out or not.  The logic does not feedback
    // to the BMSender delegate at this point; it waits until the
    // HTTP request-response cycle is finished.

    [conState setStatusCode:[(NSHTTPURLResponse *)response statusCode]];

#ifdef SHOULD_LOG
    NSLog(@"s; status code arrived for <<%@>> : %d",[conState messageId],(int)[conState statusCode]);
#endif
}

// implementation of method from NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection
    didReceiveData:(NSData *)data {

    // in a more sophisticated example, the code might capture the
    // XML response data here, parse it and store it into the
    // connection state.  It would then be able to provide richer
    // feedback to the client.
}

// implementation of method from NSURLConnectionDelegate
- (NSURLRequest *)connection:(NSURLConnection *)connection
             willSendRequest:(NSURLRequest *)request
            redirectResponse:(NSURLResponse *)redirectResponse {
    return request;
}

// implementation of method from NSURLConnectionDelegate
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    BMSenderConnectionState *conState = [self senderConnectionInTransitForMessageIdentifiedBy:connection];
    ERRCHECK_UNKNOWNCONNECTION

#ifdef SHOULD_LOG
    NSLog(@"s; connection complete for <<%@>>",[conState messageId]);
#endif

    // in a prior NSURLConnectionDelegate method, the HTTP status code
    // from the HTTP response was captured.  Here the logic is able to
    // use that HTTP status code and invoke the appropriate feedback
    // method on the BMSender's delegate.

    switch([conState statusCode]) {
        case 200:
        case 204:
            [[conState delegate] successfullySentMessageIdentifiedBy:[conState messageId]];
            break;

        default:
            [[conState delegate] failedToSendMessageIdentifiedBy:[conState messageId]];
            break;
    }

    [_connectionStatuses removeObject:conState];

}

// #########################################################

@end
BMSender.h

Now finally, modify the main.m. This contains a small test program to demonstrate the use of the BMSender class above and provides a small inline Objective-C class that demonstrates the delegate mechanic for discovering if messages were send successfully or failed.

#import <Foundation/Foundation.h>
#import "BMSender.h"

// #########################################################
#pragma mark Example Sender Delegate

/*
 This class implements the BMSenderDelegate protocol in order
 to capture feedback about messages that were sent
 successfully or failed to be sent.  The implementation here
 is very simple and just logs that a message was or was not
 sent.  It also maintains a counter so that the run-loop runs
 until the count of outstanding messages are sent; this logic
 is specific to this test-harness.
 */

@interface BMExampleSenderDelegate : NSObject <BMSenderDelegate> {
    NSInteger _outstanding_messages;
}

- (void) incrementOutstandingMessages;
- (void) decrementOutstandingMessages;
- (BOOL) hasOutstandingMessages;

- (void) successfullySentMessageIdentifiedBy:(NSString *)messageId;
- (void) failedToSendMessageIdentifiedBy:(NSString *)messageId;

@end

@implementation BMExampleSenderDelegate

- (id) init {
    [super init];
    _outstanding_messages = 0;
    return self;
}

- (void) incrementOutstandingMessages { _outstanding_messages++; }
- (void) decrementOutstandingMessages { _outstanding_messages--; }
- (BOOL) hasOutstandingMessages { return _outstanding_messages; }

- (void) successfullySentMessageIdentifiedBy:(NSString *)messageId {
    [self decrementOutstandingMessages];
    NSLog(@"d; successfully sent message; %@",messageId);
}

- (void) failedToSendMessageIdentifiedBy:(NSString *)messageId {
    [self decrementOutstandingMessages];
    NSLog(@"d; failed to send message; %@",messageId);
}

@end

// #########################################################
#pragma mark Syntax

static void syntax(int argc, const char * argv[]) {
    fprintf(stderr,"%s <username> <password> <msisdn> <body>\n",argv[0]);
    fprintf(stderr,"username : your api username\n");
    fprintf(stderr,"password : your api password\n");
    fprintf(stderr,"msisdn   : the phone number to send to such as 64222222221\n");
    fprintf(stderr,"body     : the message to send to that phone number\n");
}

// #########################################################
#pragma mark Test Program

int main (int argc, const char * argv[]) {

    // If there are not the right number of arguments then
    // bail out.  Ideally more argument checks would be
    // done here.

    if(5 != argc) {
        syntax(argc,argv);
        return 1;
    }

    @autoreleasepool {

        NSString *username = [NSString stringWithUTF8String:argv[1]];
        NSString *password = [NSString stringWithUTF8String:argv[2]];
        NSString *msisdn = [NSString stringWithUTF8String:argv[3]];
        NSString *body = [NSString stringWithUTF8String:argv[4]];
        NSString *messageId = [NSString stringWithFormat:@"%@-%d",msisdn,random()];

        // the delegate is configured to capture the result of the
        // message send.

        BMExampleSenderDelegate *delegate = [[BMExampleSenderDelegate alloc] init];

        // setup a sender to send messages.  This object is configured
        // with your username and password.

        BMSender *sender = [[BMSender alloc] initWithUserId:username
                                              password:password];

        // actually send a message and track in the delegate that one
        // message is expected to be sent.

        NSLog(@"m; will dispatch message");
        [sender sendMessage:body to:msisdn identifiedBy:messageId delegate:delegate];
        [delegate incrementOutstandingMessages];
        NSLog(@"m; did dispatch message");

        // loop until there are no more outstanding messages.  In a
        // regular Cocoa application, this run-loop would be operated
        // by the application environment and you would not be wanting
        // to exit the process once the message were sent.

        NSLog(@"m; run-loop running until message sent...");
        while([delegate hasOutstandingMessages]) {
            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
        }
        NSLog(@"m; ...all messages sent; will stop running run-loop");

        // clean-up.

        NSLog(@"m; clean-up");
        [sender release];
        [delegate release];

    }
    return 0;
}
main.m

Configure the Launch Arguments

Assuming that the X-Code environment is used to launch the program, a number of command-line arguments will be required. You can access those from; Product →Edit Scheme... →Run... →"Arguments Passed on Launch". Use the following arguments in the following order;

  • Your username
  • Your password
  • The phone number that you would like to send the message to — this should consist of only digits with no leading zero digits
  • The body of the message — best to enclose this in speech marks

Run the Program

Press the run button in the top-left area of the X-Code window. This will compile and start the program. The output will be written to the console and amongst the lines you should see a line similar to;

s; successfully sent message; .....

Assuming success, the message should arrive on the nominated handset.

This concludes this tutorial.