O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Network Programming: Chapter 7 - iPhone SDK Application Development

by Jonathan Zdziarski

This chapter has been lengthened and enhanced with an entirely new example.


One of the greatest strengths of the iPhone is its ability to deliver advanced functionality over an "always on" Internet connection. While the iPhone supports the usual set of standard C functions for network programming, Apple has also provided a framework, named CFNetwork, to provide name resolution, socket connectivity, and basic protocol communications.
iPhone SDK Application Development

This excerpt is from iPhone SDK Application Development. This practical book offers the knowledge and code you need to create mobile applications and games for the iPhone and iPod Touch, using the Apple SDK.

buy button

You might consider using CFNetwork instead of standard BSD sockets if your application has a need for a run-loop; that is, you'll be able to use CFNetwork without spinning up threads or writing your own polling routines. The CFNetwork framework also adds an easy mechanism to handle reading and writing through socket streams, and supports common protocols such as HTTP and FTP out of the box. This allows you to focus on the more important aspects of your code, relieving you from the need to support individual protocols. CFNetwork is also supported in Leopard, meaning you can easily port your networking code to the desktop.

In addition to the CFNetwork framework, the iPhone's foundation framework supports objects such as NSURLRequest, which can be used to directly load web objects from the Internet. These simple objects provide callbacks, allowing your application to simply request an object and receive a notification later on, once it's loaded.

To use the CFNetwork frameworks, you'll need to add it to your Xcode project. Right-click on the Frameworks folder in your project, and then choose Add Framework. Navigate to the CFNetwork.framework folder, and then click Add.

Basic Sockets Programming

The most common use of the CFNetwork framework is to communicate across network sockets. A socket is a single channel of communication between two endpoints; think of it as the string connecting two tin cans. Sockets can be established between computers across a network (such as an HTTP connection between the iPhone and a web server), or locally between applications on the same device (such as two programs sharing data). The end responsible for initiating the connection is commonly referred to as the client, while the endpoint that receives and services the connection is considered the server. Another type of paradigm is peer-to-peer networking. Peer-to-peer networks employ ad-hoc connections between standalone client machines to share resources among each other, rather than connecting to a centralized server for the resources. In many situations, however, a central server is used to coordinate peers.

Socket Types

There are two primary types of sockets: UDP and TCP.

A UDP (User Datagram Protocol) socket is used for sending short messages called datagrams to the recipient. Datagrams are single packets of data that are sent and received without any "return postage." There is no guarantee that the recipient will receive a particular packet, and multiple packets may be received out of order. Datagrams are generally thought of as unreliable, in the same way that a carrier pigeon can be unreliable. This form of communication is used for sending short query/response-type messages that do not require authentication, such as DNS (name resolution) lookups, as well as by some protocols where lost packets are irrelevant; such as live video streams and online multiplayer games, where an interruption can be ignored.

The other type of socket, TCP (Transmission Control Protocol), is much more commonly used, as it provides the framework for a complete, structured "conversation" to occur between the two endpoints. TCP connections provide a means to ensure the message was received, and guarantees that packets are received in order. TCP is used by protocols including HTTP, FTP, and others where data must be reliably sent and received in order. To keep track of the ordering of packets, TCP employs a sequence number, which identifies the sequence of each packet. This not only keeps your conversation in order, but also adds a basic level of protection against some forms of spoofing (data forgery by a malicious party).

CFSocket

If the original C-based socket functions were written specifically for Mac OS X, they'd look more like CFSocket. CFSocket bears a striking resemblance to their traditional C counterparts, but with the added flexibility of taking advantage of many OS X design features, such as memory allocators and callbacks. The CFSocket object is the base structure used to create UDP and TCP sockets, and is used to set up, communicate over, and break down a connection. Because the CFNetwork framework is C-based, CFSocket is a structure, rather than a class, and all calls to its functions will include the socket as the first argument.

Creating New Sockets

You can use the CFSocketCreate function to create both TCP and UDP sockets. This function is provided with the granular information needed to build a socket, including the type of memory allocator to use, protocol family (IPv4 or IPv6), and type of socket. A socket can perform its function in the background of an application by scheduling it on a run loop, so you'll also be able to supply a callback function to be invoked during certain socket events. This allows you to focus on your application, rather than writing your own run loop to wait for connections and data. The prototype for CFSocketCreate follows.

CFSocketRef CFSocketCreate (
    CFAllocatorRef allocator,
    SInt32 protocolFamily,
    SInt32 socketType,
    SInt32 protocol,
    CFOptionFlags callBackTypes,
    CFSocketCallBack callout,
    const CFSocketContext *context
);
CFAllocatorRef allocator

Specifies the type of memory allocator to create the new socket. Use NULL or kCFAllocatorDefault to use the default. Other types of allocators include kCFAllocatorSystemDefault, the system's default allocator (which Apple strongly recommends against using), kCFAllocatorMalloc (which uses malloc(), realloc() and free()), kCFAllocatorMallocZone (which allocates space in unscanned memory), and kCFAllocatorNull (which does not allocate or deallocate memory; useful for cases when data should not be deallocated). You'll almost always want to use the default allocator unless your program calls for a very specific alternative.

SInt32 protocolFamily

The protocol family for the socket. The default is PF_INET (Internet Protocol also known as IPV4). For IPv6 sockets, use PF_INET6.

SInt32 socketType

Identifies the socket type. Use SOCK_STREAM for TCP sockets or SOCK_DGRAM for datagram (UDP) sockets.

SInt32 protocol

The protocol to be used for the socket. This can be one of IPPROTO_TCP or IPPROTO_UDP and should match the socket type specified.

CFOptionFlags callBackTypes

A CFSocket run loop can invoke callbacks for different types of socket events. For example, a callback can be issued when a socket connects, or when there is data available. You construct the callBackTypes argument using a bitmask, which you can set using a bitwise-OR operation. Apple's prototype defines the following enumeration of flags.

enum CFSocketCallBackType {
    kCFSocketNoCallBack = 0,
    kCFSocketReadCallBack = 1,
    kCFSocketAcceptCallBack = 2,
    kCFSocketDataCallBack = 3,
    kCFSocketConnectCallBack = 4,
    kCFSocketWriteCallBack = 8
};
typedef enum CFSocketCallBackType CFSocketCallBackType;
CFSocketCallBack callout

Specifies the function that should be called when one of the events identified in callBackTypes is triggered. Instead of writing your own run loop to wait for connections and send and receive data, using a run loop would instead call this function whenever one of the desired events occurs.

const CFSocketContext *context

A special structure containing a CFSocket's context, which can encapsulate a pointer to your own user-defined data for the socket. You'll learn about this in the next section.

CFTimeInterval timeout

Specifies the time to wait for a connection. If you supply a negative value, the connection will be established in the background, allowing your program to continue running. The callback will then be invoked when the connection is made.

Creating Sockets from Existing Sockets

You can create a CFSocket object from an existing native socket by using the CFSocketCreateWithNative function. This function is very similar to the CFSocketCreate function, but accepts the existing socket as one of its arguments. This may be useful in cases where you have legacy C code to build a socket, and want to plug it into the CFNetwork framework. The prototype follows.

CFSocketCreateWithNative (
   CFAllocatorRef allocator,
   CFSocketNativeHandle sock,
   CFOptionFlags callBackTypes,
   CFSocketCallBack callout,
   const CFSocketContext *context
);

In the example above, you'll immediately notice that the native socket argument isn't defined as an int, which is the standard on most systems, but rather a CFSocketNativeHandle. The CFSocketNativeHandle data type is defined as the operating system's native data type for C-based sockets, so this will typically resolve to an int on most systems.

Socket Functions

Once you have created the socket, you can perform a number of functions on it. You can use functions that are specific to the CFNetwork framework, and with a special function named CFSocketGetNative, you can also operate on the socket's lower-level native socket to perform native C-socket functions. The following relevant functions are available on CFSocket objects.

CFSocketGetNative

Returns the system's native socket, on which you can perform the native set of C-based socket operations. This is usually an int data type on most systems. In the example to follow, you'll see it call the native setsockopt() function. This allows you to plug in the CFNetwork framework without sacrificing any functionality, while maintaining compatibility with legacy code.

CFSocketConnectToAddress

Invokes a connect request on the local socket. This is used to connect the socket to a listening (server) socket, such as a web server.

CFSocketCopyAddress

Returns the local address of the CFSocket. This is useful in determining what IP address or addresses your socket is listening on.

CFSocketCopyPeerAddress

Returns the address of the remote socket that the CFSocket is connected to. This provides the IP address of the remote end of the connection, for events when your application is acting as the server.

CFSocketCreateRunLoopSource

Creates a run loop source object for a CFSocket object. You'll see how this works in the example to follow.

Enabling and disabling callbacks

Callbacks for CFSocket objects can be enabled or disabled at the programmer's discretion. This is useful in cases where the callback behavior for a socket changes depending on the socket's status. You can do this using the CFSocketDisableCallBacks and CFSocketEnableCallBacks functions.

Both functions accept a socket and a bitwise-OR'd set of callback flags as arguments, matching the callback flags you've already learned. To disable accept and read callbacks for a given socket, your code might look like the example below.

CFSocketDisableCallBacks(mySocket,
    kCFSocketAcceptCallBack | kCFSocketReadCallback);

To re-enable them, simply swap the function name to CFSocketEnableCallBacks, as shown below.

CFSocketEnableCallBacks(mySocket,
    kCFSocketAcceptCallBack | kCFSocketReadCallback);

Sending data

The CFNetwork framework provides an abstracted routine for sending data, which helps simplify the process. To send data, use the CFSocketSendData command.

char joke[] = "Why did the chicken cross the road?";
kCFSocketError err = CFSocketSendData(mySocket, joke, (strlen(joke)+1), 10);

You can then check the error code to determine if data was sent successfully.

if (err == kCFSocketSuccess) {
    /* Success */
}

The CFNetwork framework uses the following error codes.

kCFSocketSuccess

Operation succeeded

kCFSocketError

Operation failed

kCFSocketTimeout

Operation timed out

Callbacks

You can set certain events to trigger callbacks, such as incoming data or new connections. This allows you to write software that doesn't need to block or loop itself to check the status of the socket. CFNetwork uses a standard callback form factor for all callback functions, providing whatever data is relevant to the type of callback. The prototype for this function follows.

typedef void (*CFSocketCallBack) (
   CFSocketRef s,
   CFSocketCallBackType callbackType,
   CFDataRef address,
   const void *data,
   void *info
);

The following information is provided with each callback. Some information may differ, depending on the type of callback being sent.

CFSocketRef s

The CFSocket corresponding to the event that occurred. This allows your callback function to support multiple sockets.

CFSocketCallBackType callbackType

The enumerated value for the callback, identifying what kind of event has occurred. See the list of callback types from earlier.

CFDataRef address

A CFData object containing the lower-level sockaddr information. You can use this to obtain the remote address to which the socket is connected. This is only provided during accept and data callbacks.

const void *data

A pointer to special data that is relevant to the callback. For a data event, a CFData object is passed containing the received data. For an accept event, a pointer to a CFSocketNativeHandle is provided, pointing to the native socket object. For a connect event, a pointer to an SInt32 error code will be provided.

void *info

The pointer supplied to the CFSocketContext structure associated with the socket. This will contain any user-defined data you've associated with the socket.

CFSocketContext

Because CFSockets can run in the background in a run loop, keeping track of the data that is associated with each connection can become tricky. For example, if you are writing a search engine, you might create hundreds of connections to various web servers, and will need to know which connection is associated with which callback events. The CFSocketContext object allows you to tie a pointer to any such proprietary information to a socket structure so that it is available whenever a callback is triggered.

The context structure prototype follows.

struct CFSocketContext {
    CFIndex version;
    void *info;
    CFAllocatorRetainCallBack retain;
    CFAllocatorReleaseCallBack release;
    CFAllocatorCopyDescriptionCallBack copyDescription;
};
typedef struct CFSocketContext CFSocketContext;
CFIndex version

The version number of the structure. Apple insists this be set to 0.

void *info

A pointer to your application's user-defined data, which will be associated with the CFSocket object when it is created. This pointer will be passed in the arguments list of all callbacks issued by the CFSocket object.

CFAllocatorRetainCallBack retain

An optional callback used when the context is retained. Use NULL if no callback is necessary.

CFAllocatorReleaseCallBack release

An optional callback used when the context is released. Use NULL if no callback is necessary.

CFAllocatorCopyDescriptionCallBack copyDescription

An optional callback invoked when the object is copied into another context. Use NULL if no callback is necessary.

An example socket context follows.

char joke[] = "Why did the chicken cross the road?";
CFSocketContext CTX = { 0, joke, NULL, NULL, NULL };

When operating on a socket, you can obtain the socket's context by calling CFSocketGetContext. This function copies the contents of the socket's context into a local structure provided by the caller.

CFSocketContext localCTX;
CFSocketGetContext(mySocket, &localCTX);

Socket Streams

Socket streams provide an easy interface for reading and writing data to or from a socket. Each socket can be bound to a read and write stream, allowing for synchronous or asynchronous communication. Streams encapsulate most of the work needed for reading and writing byte streams, and replace the traditional send() and recv() functions used in C. Two different stream objects are used with sockets: CFReadStream and CFWriteStream.

Read streams

A special set of CFReadStream functions allow for simple read operations on a socket. A read buffer is used, much like in C, in which the read stream is looped until the desired number of bytes are read.

char buffer[1024];
CFIndex bytes_recvd;
int recv_len = 0;

memset(buffer, 0, sizeof(buffer));
while (!strchr(buffer, '\n') && recv_len < sizeof(buffer)) {
    bytes_recvd = CFReadStreamRead(readStream, buffer + recv_len,
        sizeof(buffer) – recv_len);
    if (bytes_recvd < 0) {
        /* Error has occurred. Close the socket and return. */
    }
    recv_len += bytes_recvd;
}

A list of useful CFReadStream functions follows.

CFReadStreamOpen, CFReadStreamClose

Opens and closes a read stream. These functions allocate and release the resources needed to perform stream reads. A stream must first be opened before it can be attached to a socket.

CFReadStreamRead

The actual read function of the read stream. This function performs reading from the attached socket into the provided buffer, returning the number of bytes that have been read. Much like traditional sockets, the read can be looped until the desired number of bytes has been received.

CFReadStreamGetBuffer

Returns a pointer to the read stream's internal buffer of unread data, allowing the implementer to access the raw buffer directly.

CFReadStreamGetStatus

Returns the current status of the read stream. The status will be one of the following.

  • kCFStreamStatusNotOpen (read stream is not open)

  • kCFStreamStatusOpening (read stream is being opened for reading)

  • kCFStreamStatusOpen (read stream is open and ready)

  • kCFStreamReading (the stream is currently being read from)

  • kCFStreamStatusAtEnd (no more data is available to read from the stream)

  • kCFStreamStatusClosed (the read stream has been closed)

  • kFStreamStatusError (an error has occurred on the read stream)

CFReadStreamHasBytesAvailable

Returns a Boolean value indicating whether incoming data is ready to be read without blocking. You can use this to periodically poll the socket about whether data is available, although this will be unnecessary if you are using a run loop.

CFReadStreamSheduleWithRunLoop, CFReadStreamUnscheduleFromRunLoop

Used to schedule or unscheduled the stream into or from a run loop. Once scheduled, the loop client is called during certain events, such as opening, errors, and when data is available to be read. You can also schedule a single stream with multiple run loops and modes.

CFReadStreamSetClient

Asigns a client to receive callback for the stream while in the run loop.

If a socket already exists, the CFStreamCreatePairWithSocket function can automatically initialize a read and write stream.

/* The native socket, used for various operations */
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;

CFSocketNativeHandle sock = *(CFSocketNativeHandle *) data;
CFStreamCreatePairWithSocket(kCFAllocatorDefault, sock,
&readStream, &writeStream);

To schedule a read stream onto the run loop, call its scheduler function. This will cause the actual operation of the read stream to be threaded into the background, and will activate its callback functions.

CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(),
        kCFRunLoopCommonModes);

When a read stream has been entered into a run loop, the client's callback functions are called whenever certain events occur.

typedef void (*CFReadStreamClientCallBack) (
   CFReadStreamRef stream,
   CFStreamEventType eventType,
   void *clientCallBackInfo
);

Similar to the CFSocket callback function, the stream callbacks provide information about the type of event and relevant data for the event. You can invoke a callback for the following events.

kCFStreamEventNone

Undefined event.

kCFStreamEventOpenCompleted

The stream has been successfully opened and is ready for further operations.

kCStreamEventHasBytesAvailable

Data is available for the stream to read.

kCFStreamEventErrorOccurred

An error has occurred on the read stream.

kCFStreamEventEndEncountered

An EOF (end-of-file) has been reached, and there is no more data available on the stream.

Write streams

The complement to a read stream is a CFWriteStream. This stream is designated for writing, and manages the sending of data through a CFSocket. An example follows.

char data[] = "Beware the Jabberwocky.\n";
m CFIndex bytes_sent = 0;
int send_len = 0;

if (CFWriteStreamCanAcceptBytes(writeStream)) {
    bytes_sent = CFWriteStreamWrite(writeStream, data + send_len,
        (strlen(data)+1) - send_len);
    if (bytes_sent < 0) {
        /* Send error occurred. Close the socket and return. */
    }
    send_len += bytes_sent;
}

A list of useful CFWriteStream functions follows.

CFWriteStreamOpen, CFWriteStreamClose

Opens and closes a write stream. These functions allocate and release the resources needed to perform stream writes. A stream must first be opened before it can be attached to a socket.

CFWriteStreamWrite

The actual write function for the write stream. This function performs the writing from a data source to the socket it is paired with, returning the number of bytes that have been sent. Much like traditional send functions performed on C sockets, the write can be looped until the desired number of bytes has been sent.

CFWriteStreamGetStatus

Returns the current status of the write stream. The status will be one of the following.

  • kCFStreamStatusNotOpen (write stream is not open)

  • kCFStreamStatusOpening (write stream is being opened for writing)

  • kCFStreamStatusOpen (write stream is open and ready)

  • kCFStreamWriting (the stream is currently being written to)

  • kCFStreamStatusAtEnd (no more data can be written to the stream)

  • kCFStreamStatusClosed (the write stream has been closed)

  • kFStreamStatusError (an error has occurred on the write stream)

CFReadStreamCanAcceptBytes

Returns a Boolean value indicating whether the stream can be written to.

CFWriteStreamSheduleWithRunLoop, CFWriteStreamUnscheduleFromRunLoop

Used to schedule or unschedule the stream into or from a run loop. After scheduling, the loop client is called during certain events, such as opening, errors, and when data is written. You can also schedule a single stream with multiple run loops and modes.

CFWriteStreamSetClient

Assigns a client to receive callback for the stream while in the run loop.

To schedule a write stream onto the run loop, call its scheduler function. This will cause the actual operation of the write stream to be threaded into the background, and will activate its callback functions.

CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(),
        kCFRunLoopCommonModes);

When a write stream has been entered into a run loop, the client's callback functions are called whenever certain events occur.

typedef void (*CFWriteStreamClientCallBack) (
   CFWriteStreamRef stream,
   CFStreamEventType eventType,
   void *clientCallBackInfo
);

Similar to the CFSocket callback function, the stream callbacks provide information about the type of event and relevant data for the event. A callback may be invoked for the following events.

kCFStreamEventNone

Undefined event.

kCFStreamEventOpenCompleted

The stream has been successfully opened and is ready for further operations.

kCStreamEventCanAcceptBytes

The stream is ready for writing.

kCFStreamEventErrorOccurred

An error has occurred on the read stream.

kCFStreamEventEndEncountered

An EOF (end-of-file) has been reached, and there is no more data available on the stream.

CFSocket Example: Lame Joke Client/Server

In this example, you'll put together your knowledge of the CFSocket and CFSocketContext structures, as well as streams to build a TCP server program that will tell the lame punch line to any joke asked of it. You'll store the joke's punch line in the CFSocketContext on the server side and provide the answer to the client. You'll notice an eerie similarity to standard C sockets programming, however the server example will show off the CFNetwork framework's run loop capabilities, which make socket management much easier.

#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#include <sys/socket.h>
#include <netinet/in.h>
#if TARGET_OS_IPHONE
#import <CFNetwork/CFNetwork.h>
#endif

/* Port to listen on */
#define PORT 2048

void AcceptCallBack(CFSocketRef, CFSocketCallBackType, CFDataRef,
    const void *, void *);

int main(int argc, char **argv) {
    /* The server socket */
    CFSocketRef TCPServer;

    /* The punchline to our joke */
    char punchline[] = "To get to the other side! Ha ha!\n\r";

    /* Used by setsockopt */
    int yes = 1;

    /* Build our socket context; this ties the punchline to the socket */
    CFSocketContext CTX = { 0, punchline, NULL, NULL, NULL };

   /* Create the server socket as a TCP IPv4 socket and set a callback */
    /* for calls to the socket's lower-level accept() function */
    TCPServer = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP,
        kCFSocketAcceptCallBack, (CFSocketCallBack)AcceptCallBack, &CTX);
    if (TCPServer == NULL)
        return EXIT_FAILURE;

    /* Re-use local addresses, if they're still in TIME_WAIT */
    setsockopt(CFSocketGetNative(TCPServer), SOL_SOCKET, SO_REUSEADDR,
        (void *)&yes, sizeof(yes));

    /* Set the port and address we want to listen on */
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_len = sizeof(addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    NSData *address = [ NSData dataWithBytes: &addr length: sizeof(addr) ];
    if (CFSocketSetAddress(TCPServer, (CFDataRef) address) != kCFSocketSuccess) {
        fprintf(stderr, "CFSocketSetAddress() failed\n");
        CFRelease(TCPServer);
        return EXIT_FAILURE;
    }

    CFRunLoopSourceRef sourceRef =
        CFSocketCreateRunLoopSource(kCFAllocatorDefault, TCPServer, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes);
     CFRelease(sourceRef);

    printf("Socket listening on port %d\n", PORT);

    CFRunLoopRun();

    /* Get on with our life, instead of waiting for incoming connections. */
    while(1) {
        struct timeval tv;
        tv.tv_usec = 0;
        tv.tv_sec = 1;
        select(-1, NULL, NULL, NULL, &tv);
    }

}

Our listening socket is now set up, and a callback function will be invoked whenever a new connection is accepted. The callback function below is one example of how an incoming connection might be handled. In this example, the callback function creates and pairs a read and write stream to the socket. It then waits for the other end to tell a joke, then sends the punch line.

void AcceptCallBack(
    CFSocketRef socket,
    CFSocketCallBackType type,
    CFDataRef address,
    const void *data,
    void *info)
{
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    CFIndex bytes;
    UInt8 buffer[128];
    UInt8 recv_len = 0, send_len = 0;

    /* The native socket, used for various operations */
    CFSocketNativeHandle sock = *(CFSocketNativeHandle *) data;

    /* The punch line we stored in the socket context */
    char *punchline = info;

    /* Create the read and write streams for the socket */
    CFStreamCreatePairWithSocket(kCFAllocatorDefault, sock,
        &readStream, &writeStream);

    if (!readStream || !writeStream) {
        close(sock);
        fprintf(stderr, "CFStreamCreatePairWithSocket() failed\n");
        return;
    }

    CFReadStreamOpen(readStream);
    CFWriteStreamOpen(writeStream);

    /* Wait for the client to finish sending the joke; wait for newline */
    memset(buffer, 0, sizeof(buffer));
    while (!strchr((char *) buffer, '\n') && recv_len < sizeof(buffer)) {
        bytes = CFReadStreamRead(readStream, buffer + recv_len,
            sizeof(buffer) - recv_len);
        if (bytes < 0) {
            fprintf(stderr, "CFReadStreamRead() failed: %d\n", bytes);
            close(sock);
            return;
        }
        recv_len += bytes;
    }

    /* Send the punchline */
    while (send_len < (strlen(punchline+1))) {
        if (CFWriteStreamCanAcceptBytes(writeStream)) {
            bytes = CFWriteStreamWrite(writeStream,
                (unsigned char *) punchline + send_len,
                (strlen((punchline)+1) - send_len) );
            if (bytes < 0) {
                fprintf(stderr, "CFWriteStreamWrite() failed\n");
                close(sock);
                return;
            }
            send_len += bytes;
        }
        close(sock);
        CFReadStreamClose(readStream);
        CFWriteStreamClose(writeStream);
        return;
    }
}

To build and run this example for the Mac OS X command-line, paste both code snippets into a file named server.m, then issue the following commands from a terminal window:

$ gcc -o jokeserver server.m -framework CoreFoundation -framework Foundation –lobjc
$ ./jokeserver

Once the joke server is running, open up a new terminal window. The client example to follow will use CFNetwork to establish a connection; using a ConnectCallBack function, called then a connection is made. This example will then use standard send and recv C-based functions to illustrate how standard socket operations can be performed on a CFSocket using the CFSocketNativeHandle function. Note the use of the CFSocketConnectToAddress function to establish a connection.

#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#include <sys/socket.h>
#include <netinet/in.h>
#if TARGET_OS_IPHONE
#import <CFNetwork/CFNetwork.h>
#endif

#define PORT 2048

void ConnectCallBack(CFSocketRef, CFSocketCallBackType, CFDataRef,
const void *, void *);

int main(int argc, char **argv) {

    /* The server socket */
    CFSocketRef TCPClient;

    /* The joke */
    char joke[] = "Why did the chicken cross the road?\n\r";

    /* Used by setsockopt */
    int yes = 1;

    /* Build our socket context; this ties the joke to the socket */
    CFSocketContext CTX = { 0, joke, NULL, NULL, NULL };

    /* Create the server socket as a TCP IPv4 socket and set a callback */
    /* for calls to the socket's lower-level connect() function */
    TCPClient = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP,
        kCFSocketConnectCallBack, (CFSocketCallBack)ConnectCallBack, &CTX);
    if (TCPClient == NULL)
        return EXIT_FAILURE;

    /* Set the port and address we want to listen on */
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_len = sizeof(addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    CFDataRef connectAddr = CFDataCreate(NULL, (unsigned char *)&addr, sizeof(addr));

    CFSocketConnectToAddress(TCPClient, connectAddr, −1);
    CFRunLoopSourceRef sourceRef =
        CFSocketCreateRunLoopSource(kCFAllocatorDefault, TCPClient, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes);
    CFRelease(sourceRef);
    CFRunLoopRun();

    /* Get on with our life, instead of waiting for a connect. */
    while(1) {
        struct timeval tv;
        tv.tv_usec = 0;
        tv.tv_sec = 1;
        select(-1, NULL, NULL, NULL, &tv);
    }

}


void ConnectCallBack(
    CFSocketRef socket,
    CFSocketCallBackType type,
    CFDataRef address,
    const void *data,
    void *info)
{
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    CFIndex bytes;
    UInt8 buffer[128];
    UInt8 recv_len = 0, send_len = 0;

    /* The native socket, used for various operations */
    CFSocketNativeHandle sock = CFSocketGetNative(socket);

    /* The joke we stored in the socket context */
    char *joke = info;

    printf("OK. %s", joke);

    send(sock, joke, strlen(joke)+1, 0);
    recv(sock, buffer, sizeof(buffer), 0);
    printf("%sHow lame!\n", buffer);

    close(sock);
    exit(EXIT_SUCCESS);
}

To build and run the client from the Mac OS X command-line, paste this code into a file named client.m, then issue the following commands from a terminal window:

$ gcc -o jokeclient client.m -framework CoreFoundation -framework Foundation –lobjc

With the server running, run the client from the command line. You should see the results of the exchange between the joke client and server.

$ ./jokeclient
OK. Why did the chicken cross the road?
To get to the other side! Ha ha!
How lame!

Further Study

  • Have a look at CFSocket.h, CFStream.h, and CFSocketContext.h. You'll find these deep within your SDK's Core Foundation headers in /Developer/Platforms/iPhoneOS.platform.

CFHTTP and CFFTP

The CFNetwork framework provides easy APIs for making HTTP and FTP requests. You can make these requests through CFHTTP and CFFTP APIs. Rather than programming your application to speak a particular protocol, these APIs allow the framework to do all of the dirty work on your behalf, returning the raw protocol in a serialized form, which you can then send through a write stream connected to your desired host, or connect directly to a stream to open.

While many Cocoa classes, such as NString and NSData, allow you to initialize objects with the contents of URLs, the CFHTTP and CFFTP APIs provide more granular control over the protocol layer, as you'll see in the examples to follow.

CFHTTP

You can use the CFHTTP API to create an HTTP request. This allows you to easily invoke HTTP GET, HEAD, PUT, POST, and most other standard requests. Creating a request involves the three-step process of creating the request object, defining the HTTP request message and headers, and serializing the message into raw protocol. Only HTTP POST requests generally contain a message body, which can contain POST form data to send. All other requests use an empty body while embedding the request parameters into the headers.

In the example below, an HTTP/1.1 GET request is created, specifying the URL http://www.oreilly.com and setting the Connection header to instruct the remote end to close the connection after sending data.

CFStringRef requestHeader = CFSTR("Connection");
CFStringRef requestHeaderValue = CFSTR("close");
CFStringRef requestBody = CFSTR("");

CFStringRef url = CFSTR("http://www.oreilly.com");
CFStringRef requestMethod = CFSTR("GET");

CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);
CFHTTPMessageRef request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
    requestMethod, requestURL, kCFHTTPVersion1_1);
CFHTTPMessageSetBody(request, requestBody);
CFHTTPMessageSetHeaderFieldValue(request, requestHeader, requestHeaderValue);

CFDataRef serializedRequest = CFHTTPMessageCopySerializedMessage(request);

The resulting pointer to a CFData structure provides the raw HTTP protocol output, which you would then send through a write stream to the destination server. In the example below, an HTTP GET request is created and opened through a read stream. As data flows in, the read stream's callbacks would normally be invoked to receive the new data.

int makeRequest(const char *requestURL)
{
    CFReadStream readStream;
    CFHTTPMessageRef request;
    CFStreamClientContext CTX = { 0, NULL, NULL, NULL, NULL };

    NSString* requestURLString = [ [ NSString alloc ] initWithCString:
        requestURL ];
    NSURL url = [ NSURL URLWithString: requestURLString ];

    CFStringRef requestMessage = CFSTR("");

    request = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("GET"),
        (CFURLRef) url, kCFHTTPVersion1_1);
    if (!request) {
        return −1;
    }
    CFHTTPMessageSetBody(request, (CFDataRef) requestMessage);
    readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request);
    CFRelease(request);

    if (!readStream) {
        return −'1;
    }

    if (!CFReadStreamSetClient(readStream, kCFStreamEventOpenCompleted |
                                           kCFStreamEventHasBytesAvailable |
                                           kCFStreamEventEndEncountered |
                                           kCFStreamEventErrorOccurred,
        ReadCallBack, &CTX))
    {
        CFRelease(readStream);
        return −1;    }

        /* Add to the run loop */
    CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(),
        kCFRunLoopCommonModes);

    if (!CFReadStreamOpen(readStream)) {
        CFReadStreamSetClient(readStream, 0, NULL, NULL);
        CFReadStreamUnscheduleFromRunLoop(readStream,
            CFRunLoopGetCurrent(),
            kCFRunLoopCommonModes);
        CFRelease(readStream);
        return −1;
    }

    return 0;
}

CFFTP

The CFFTP API is similar to the CFHTTP API, and relies on read streams to transmit FTP data. To create an FTP request, use the CFReadStreamCreateWithFTPURL function, as shown below. This will create the initial read stream to the FTP server.

CFStringRef url = CFSTR("ftp://ftp.somedomain.com/file.txt");
CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);

CFReadStreamRef readStream = CFReadStreamCreateWithFTPURL(
    kCFAllocatorDefault, requestURL);

Once you have created the read stream, you can tie a callback function to it so that a read function will be called when data is ready.

CFReadStreamSetClient(
    readStream,
    kCFtreamEventHasBytesAvailable,
    readCallBack,
    NULL);

The callback function will be called whenever data is available, and will be able to read the data off the stream.

void readCallBack(
    CFReadStreamRef stream,
    CFStreamEventType eventType,
    void *clientCallBackInfo)
{
    char buffer[1024];
    CFIndex bytes_recvd;
    int recv_len = 0;

    while(recv_len < total_size && recv_len < sizeof(buffer)) {
        bytes_recvd = CFReadStreamRead(stream, buffer + recv_len,
            sizeof(buffer) – recv_len);

        /* Write bytes to output or file here */
    }
    recv_len += bytes_recvd;
}

You can now schedule the request in your main program's run loop, As the read stream is presented with data, your read callback will be invoked and will continue to perform on the data until the connection is closed or until the file has been completed.

CFReadStreamScheduleWithRunLoop(readStream,
    CFRunLoopGetCurrent(), kCFRunLoopCommonModes);

Further Study

  • Have a look at CFFTPStream.h, CFHTTPStream.h, and CFNetwork.h. You'll find these deep within your SDK's Core Foundation headers in /Developer/Platforms/iPhoneOS.platform.

Fetching Web Objects: NSURLConnection

The CFNetwork framework is useful for the low-level engineering of sockets and proprietary communication, but most applications need only create simple requests for objects such as an image or other file from the Internet. Cocoa's foundation class incorporates an object called NSURLConnection to service these basic requests without having to engineer sockets or speak HTTP yourself.

The simplified use of NSURLConnection allows you to request an object using a URL. The runtime will then perform this request in the background, allowing your application to continue without blocking. When the object has loaded, a callback is used to notify your application. This is much more convenient than the initWithContentsOfURL method you've seen many foundation classes support, as it allows your application to queue up several requests without creating any perceptible delay for the user.

Creating an NSURLConnection

Creating the web request involves three separate objects.

An NSURL object is first created to contain the actual URL to the data you're trying to load.

NSURL *url = [ NSURL URLWithString: @"http://www.yourwebsite.com/image.png" ];

The NSURL object is then used to initialize an NSURLRequest object, which specifies properties of a request, such as the timeout period, caching options, and so on.

NSURLRequest *request = [ [ NSURLRequest alloc ] initWithURL: url
    cachePolicy: NSURLRequestReturnCacheDataElseLoad
    timeoutInterval: 60.0
];

The following cache policies can be used:

NSURLRequestUseProtocolCachePolicy

The default policy for caching, uses whatever the default caching policy is for a given protocol.

NSURLRequestReloadIgnoringLocalCacheData

Indicates that the object should be loaded regardless of whether it is cached locally. In other words, ignore any locally cached data.

NSURLRequestReloadIgnoringLocalAndRemoteCacheData

Indicates that not only should the data be loaded, but that caches should be instructed to discard their cache of the particular object.

NSURLRequestReturnCacheDataElseLoad

Indicates that the cached data is preferred, if available, regardless of its age or expiration date, and that the data should only be loaded if it does not exist in the cache.

NSURLRequestReturnCacheDataDontLoad

Indicates that the data in the cache, regardless of age or expiration date, should be used, and if the data does not exist in the cache, to avoid loading it. Apple's documentation describes this as useful for "offline mode" behavior.

NSURLRequestReloadRevalidatingCacheData

Indicates that the cached data should be used if its age and expiration date are valid, otherwise reload the data and freshen the cache

Once the NSURLRequest object has been created, NSURLConnection object can finally be created and a request can be initiated using the request object you've just made. An NSMutableData structure is also created as storage for the data, which will be filled as the object's data begins to arrive in your application.

NSMutableData *data = [ [ NSMutableData data ] retain ];
NSURLConnection * connection = [ [ NSURLConnection alloc ]
    initWithRequest: request delegate: self ];

Control is immediately returned to your class. By specifying the connection's delegate as self, your class will receive notifications as data comes in. Four separate delegate methods are used to handle different events while the connection is in progress.

The didReceiveResponse method is invoked by the runtime when the server hosting the object has responded and is just beginning to send data. At this time, the NSMutableData object can be initialized to zero length and any other initializations your application requires can be made.

-(void)connection:(NSURLConnection *)connection didReceiveResponse:
    (NSURLResponse *)response
{
    [ data setLength: 0 ];
}

Every time new data is received from the server, the didReceiveData method is invoked and supplied with a pointer to the actual data. This can be appended into your NSMutableData object, to continually build a composite of the object as it loads.

-(void)connection:(NSURLConnection *)connection didReceiveData:
    (NSData *)incomingData
{
    [ data appendData: incomingData ];
}

Finally, when the object has finished loading entirely, or failed with an error, either the connectionDidFinishLoading or the didFailWithError delegate method is invoked.

In the example to follow, the connectionDidFinishLoading method initializes a UIImage object using the data loaded from the connection. In your actual application, this may be an image, web page, or any other type of object.

-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"w00t! my image is finished loading!");
    UIImage *myImage = [ [ UIImage alloc ] initWithData: data ];
    [ data release ];
}

-(void)connection:(NSURLConnection *)connection didFailWithError:
    (NSError *)error
{
    NSLog(@"Uh oh. My query failed with error: %@",
        [ error localizedDescription ]);
    [ data release ];
}

NSURLConnection Example: Missing Kids

One of my recent projects involved coding the official AMBER Alert application for the National Center for Missing and Exploited Children (NCMEC). Unaware of the potential of the iPhone's unique technology, I was able to easily NCMEC that a fleet of 50 million GPS-equipped iPhones could more easily and reliably report sightings of missing children by feeding this GPS sighting data into a GIS (geographic information system), where like sightings could be easily realized. We did the whole project pro-bono over a weekend of hacking, and deployed it into the AppStore as a free application.

In this example, you'll see just a small portion of the complete application and make use of some great code for performing NSURLConnection requests. In the Missing Kids example, an XML RSS feed is loaded containing the latest US nationwide missing child alerts. This feed contains the URL to an image for each missing child. As each visible table cell is brought into view, the images will be loaded in the background using the NSURLConnection method, where they will be displayed on the screen once loaded.

You might also be familiar with this type of behavior from Apple's YouTube application, which does the same thing. Using the UITableView class's visibleCells method, this example will find out what table cells your looking at whenever the table stops scrolling. It will then fire off the appropriate web requests and eventually populate the image data for each child on the screen.

As a complete application, this example is lengthier than the rest. A base class named MissingKidAlert is used to store the information for each missing child. A custom table cell class, MissingKidAlertTableViewCell, is also used to define a custom layout for the table cells.

In the real application, you'd be able to tap on a child to view their information, and report a sighting by tapping a big red button. To keep this example as small as possible, we've only included the portion that displays the table. You can download the full AMBER Alert app for free from the App Store.

Example 7.1. Main Entry Point (main.m)

#import <UIKit/UIKit.h>

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

    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, @"MissingKid_AlertAppDelegate");
    [pool release];
    return retVal;
}

Example 7.2. Application Delegate Headers (MissingKid_AlertAppDelegate.h)

#import <UIKit/UIKit.h>
#import "MissingKidsViewController.h"

@interface MissingKid_AlertAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;

    MissingKidsViewController *missingKidsViewController;
}

@property(nonatomic, retain) UIWindow *window;

@end

Example 7.3. Application Delegate Class (MissingKid_AlertAppDelegate.m)

#import "MissingKid_AlertAppDelegate.h"

@implementation MissingKid_AlertAppDelegate

@synthesize window;

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    CGRect screenBounds = [ [ UIScreen mainScreen ] bounds ];

    self.window = [ [ UIWindow alloc ] initWithFrame: screenBounds ];
    missingKidsViewController = [ [ MissingKidsViewController alloc ] init ];

    [ window addSubview: missingKidsViewController.view ];
    [ window makeKeyAndVisible ];
}

- (void)dealloc {
    [ window release ];
    [ super dealloc ];
}

@end

Example 7.4. MissingKidAlert Headers (MissingKidAlert.h)

#import <Foundation/Foundation.h>

/* MissingKidLocation */

@interface MissingKidLocation : NSObject {
    NSString *type, *street1, *street2, *province, *postalCode, *country, *city;
}

@property(copy) NSString *type, *street1, *street2,
*province, *postalCode, *country, *city;
@end

/* MissingKidPerson: Victims and Suspects */

@interface MissingKidPerson : NSObject {
    NSString *firstName, *middleName, *lastName, *weight, *height,
*hairColor, *gender, *eyeColor;
    NSString *dateOfBirth, *description;
    UIImage *photo;
    NSString *photourl;
    MissingKidLocation *location;
}

@property(assign) UIImage *photo;
@property(assign) MissingKidLocation *location;
@property(copy) NSString *firstName, *middleName, *lastName,
*weight, *height, *hairColor, *gender, *eyeColor;
@property(copy) NSString *dateOfBirth, *description;
@property(copy) NSString *photourl;
@end

/* MissingKidAlert: Encapsulating Class */

@interface MissingKidAlert : NSObject {
    NSDate *issueDate, *missingDate;
    NSString *summary, *name;
    NSString *agencyURL;
    MissingKidPerson *victim;
    NSMutableData *data;
    NSURLConnection *connection;
    int uid;
}

@property(copy) NSDate *issueDate, *missingDate;
@property(copy) NSString *summary, *name;
@property(copy) NSString *agencyURL;
@property(assign) MissingKidPerson *victim;
@property(assign) NSMutableData *data;
@property(assign) NSURLConnection *connection;
@property(assign) int uid;
@end

Example 7.5. MissingKidAlert Class (MissingKidAlert.m)

#import "MissingKidAlert.h"

/* MissingKidLocation */

@implementation MissingKidLocation
@synthesize type, street1, street2, province, postalCode, country, city;

- (void)dealloc {

    [ type release ];
    [ street1 release ];
    [ street2 release ];
    [ province release ];
    [ postalCode release ];
    [ country release ];
    [ city release ];
    [ super dealloc ];
}

@end

/* MissingKidPerson */

@implementation MissingKidPerson
@synthesize firstName, middleName, lastName, weight, height,
hairColor, gender, eyeColor;
@synthesize dateOfBirth, description;
@synthesize location, photo, photourl;

- (id)init {
    self = [ super init ];
    if (self != nil) {
          middleName = @"";
          lastName = @"";
          firstName = @"";
    }
    return self;
}

- (void)dealloc {
    [ location release ];
    [ photo release ];
    [ photourl release ];
    [ firstName release ];
    [ middleName release ];
    [ lastName release ];
    [ weight release ];
    [ height release ];
    [ hairColor release ];
    [ gender release ];
    [ eyeColor release ];
    [ dateOfBirth release ];
    [ description release ];
    [ super dealloc ];
}

@end

/* MissingKidAlert */

@implementation MissingKidAlert
@synthesize summary, name, issueDate, missingDate;
@synthesize agencyURL;
@synthesize victim;
@synthesize data, connection;
@synthesize uid;

- (id)init {
    self = [ super init ];
    if (self != nil) {
          uid = 0;
    }
    return self;
}

- (void)dealloc {
    [ summary release ];
    [ name release ];
    [ issueDate release ];
    [ missingDate release ];
    [ agencyURL release ];
    [ victim release ];
    [ super dealloc ];
}

@end

Example 7.6. MissingKidsViewController Headers (MissingKidsViewController.h)

#import <UIKit/UIKit.h>

@interface MissingKidsViewController : UIViewController
<UITableViewDelegate,UITableViewDataSource> {
    NSMutableArray *activeAlerts;
    NSURLConnection *alertsConnection;
    NSMutableData *receivedData;
    UIView *waitView;
    UIActivityIndicatorView *activityIndicator;
    UITableView *alertsTable;
    NSArray *xmlBuffer;
    int nConnections;
    BOOL fail;
}

- (void)loadAlerts;
- (void)reloadData;

@end

Example 7.7. MissingKidsViewController Class (MissingKidsViewController.m)

#import "MissingKidsViewController.h"
#import "MissingKid_AlertAppDelegate.h"
#import "MissingKidAlert.h"
#import "MissingKidsAlertTableViewCell.h"
#import "WebViewController.h"

@implementation MissingKidsViewController

- (void)initializeWaitView {
  waitView = [ [ UIView alloc ] initWithFrame: [ [ UIScreen mainScreen ] bounds ] ];
  waitView.backgroundColor = [ UIColor whiteColor ];

  activityIndicator = [ [ UIActivityIndicatorView alloc ]
initWithActivityIndicatorStyle: UIActivityIndicatorViewStyleGray ];
  activityIndicator.frame = CGRectMake(150.0, 220.0, 25.0, 25.0);
  [ activityIndicator sizeToFit ];

  [ waitView addSubview: activityIndicator ];
}

- (id)init {
  self = [ super init ];
  if (self != nil) {
        nConnections = 0;
        fail = NO;
  }
  return self;
}

- (void)loadVisibleCells {
  NSArray *cells = [ alertsTable visibleCells ];

  for (MissingKidsAlertTableViewCell *cell in cells) {
        NSIndexPath *indexPath = [ alertsTable indexPathForCell: cell ];
        MissingKidAlert *alert = [ activeAlerts objectAtIndex: [
indexPath indexAtPosition: 1 ] ];

        /* Load Image URL */
        if (alert.connection == nil && alert.victim.photo == nil &&
alert.victim.photourl != nil) {
             NSString *url = alert.victim.photourl;
             if (url != nil) {
                   NSURLRequest *request = [ [ [ NSURLRequest alloc ]
 initWithURL: [ NSURL URLWithString: url ] cachePolicy:
NSURLRequestReturnCacheDataElseLoad timeoutInterval: 60.0 ] autorelease ];
                   alert.data = [ [ NSMutableData data ] retain ];
                   alert.connection = [ [ NSURLConnection alloc ]
initWithRequest: request delegate: self ];

                   nConnections++;
                   if (nConnections == 1)
                        [ UIApplication sharedApplication
].networkActivityIndicatorVisible = YES;
             }
        }
  }
}

- (void)loadView {
  [ super loadView ];

  alertsTable = [ [ UITableView alloc ] initWithFrame: [ [ UIScreen
mainScreen ] bounds ] ];
  alertsTable.delegate = self;
  alertsTable.dataSource = self;

  [ self.view addSubview: alertsTable ];

  [ self initializeWaitView ];
  [ self.view addSubview: waitView ];

  [ self loadAlerts ];
}

- (void)loadAlerts {
  NSURL *url;
  NSURLRequest *request;

  url = [ NSURL URLWithString: @"http://www.missingkids.
com/missingkids/servlet/XmlServlet?act=rss" ];
  request = [ [ [ NSURLRequest alloc ] initWithURL: url cachePolicy:
NSURLRequestReloadIgnoringLocalAndRemoteCacheData
timeoutInterval: 60.0 ] autorelease ];

  [ alertsTable removeFromSuperview ];
  [ activityIndicator startAnimating ];
  waitView.hidden = NO;

  NSLog(@"%s querying NCMEC server", __func__);

  [ UIApplication sharedApplication ].networkActivityIndicatorVisible = YES;
  alertsConnection = [ [ NSURLConnection alloc ] initWithRequest: request
delegate: self ];
}

- (void)reloadData {
  [ self loadAlerts ];
}

/* Ugly parsing code */

- (NSArray *)getLine: (int) lineNum {
  BOOL append = NO;
  NSArray *split;
  NSMutableArray *components;
  NSRange range;
  NSString *line = nil;
  NSString *component;

  if (lineNum > [ xmlBuffer count ])
        return nil;

  components = [ [ NSMutableArray alloc ] init ];
  line = [ [ xmlBuffer objectAtIndex: lineNum ]
stringByTrimmingCharactersInSet: [ NSCharacterSet whitespaceCharacterSet ] ];
  split = [ line componentsSeparatedByString: @" " ];

  for (int i = 0; i < [ split count ]; i++) {

        if (append == NO)
             component = [ split objectAtIndex: i ];
        else
             component = [ component stringByAppendingFormat: [
NSString stringWithFormat: @" %@", [ split objectAtIndex: i ] ] ];

        range = [ component rangeOfString: @"=\"" ];
        if (range.location != NSNotFound)
             append = YES;

        if ([ component hasSuffix: @"\"" ] || [ component hasSuffix: @"\">" ] ||
 [ component hasSuffix: @"\"/>" ])
             append = NO;

        if (append == NO && component != nil) {
             [ components addObject: component ];
        }
  }

  NSArray *comp = [ NSArray arrayWithArray: components ];
  [ components removeAllObjects ];
  [ components release ];
  return comp;
}

- (void)parseXML {
  char *bytes = (char *) [ receivedData bytes ];
  NSString *xml = [ NSString stringWithCString: bytes ];

  xmlBuffer = [ [ xml stringByReplacingOccurrencesOfString: @"<"
withString :@"\n<" ] componentsSeparatedByCharactersInSet: [
NSCharacterSet newlineCharacterSet ] ];

  for (MissingKidAlert *alert in activeAlerts)
        [ alert release ];

  [ activeAlerts removeAllObjects ];
  [ activeAlerts release ];
  activeAlerts = [ [ NSMutableArray alloc ] init ];

  for(int i = 0; i < [ xmlBuffer count ]; i++) {
        NSString *line = [ xmlBuffer objectAtIndex: i ];

        if ([ line hasPrefix: @"<item>" ]) {

             MissingKidAlert *alert = [ [ MissingKidAlert alloc ] init ];
             MissingKidPerson *victim = [ [ MissingKidPerson alloc ] init ];
             victim.location = [ [ MissingKidLocation alloc ] init ];
             alert.victim = victim;

             while(!([ line isEqualToString: @"</item>" ])) {

                   if ([ line hasPrefix: @"<title>" ]) {
                        NSArray *components  = [ [ line
stringByReplacingOccurrencesOfString: @"<title>" withString: @"" ]
componentsSeparatedByString: @":" ];
                        if ([ components count ] < 2) {
                             NSLog(@"%s skipping %@", __func__, line);
                             continue;
                        }

                        NSArray *nameComponents = [ [ components
objectAtIndex: 1 ] componentsSeparatedByString: @"(" ];
                        if ([ nameComponents count ] < 2) {
                             continue;
                             NSLog(@"%s skipping %@", __func__, line);
                        }

                        victim.location.province = [ [ nameComponents
objectAtIndex: 1 ] stringByReplacingOccurrencesOfString: @")" withString: @"" ];
                        nameComponents = [ [ [ [ nameComponents
objectAtIndex: 0 ] stringByTrimmingCharactersInSet: [ NSCharacterSet
whitespaceCharacterSet ] ] capitalizedString ] componentsSeparatedByString: @" " ];
                        if ([ nameComponents count ] < 3) {
                             victim.firstName = [ nameComponents objectAtIndex: 0 ];
                             if ([ nameComponents count ] > 1)
                                   victim.lastName = [ nameComponents
objectAtIndex: 1 ];
                        } else {
                             victim.firstName = [ nameComponents objectAtIndex: 0 ];
                             victim.middleName = [ nameComponents objectAtIndex: 1 ];
                             victim.lastName = [ nameComponents objectAtIndex: 2 ];
                        }


                   } else if ([ line hasPrefix: @"<pubDate>" ]) {
                        alert.issueDate = [ NSDate
dateWithNaturalLanguageString: [ line stringByReplacingOccurrencesOfString:
@"<pubDate>" withString: @"" ] ];
                   } else if ([ line hasPrefix: @"<link>" ]) {
                        alert.agencyURL = [ line
stringByReplacingOccurrencesOfString: @"<link>" withString: @"" ];
                        NSArray *components = [ line
componentsSeparatedByString: @"=" ];
                        for(int j = 0; j < [ components count ]; j++) {
                             if ([ [ components objectAtIndex: j ]
hasSuffix: @"caseNum" ] && j < [ components count ] −1) {
                                   alert.uid = [ [ components
objectAtIndex: j + 1] intValue ];
                             }
                        }
                   } else if ([ line hasPrefix: @"<description>" ]) {
                        alert.summary = [ line
stringByReplacingOccurrencesOfString: @"<description>" withString: @"" ];
                        NSArray *components = [ line
componentsSeparatedByString: @"," ];
                        if ([ components count ] >= 3) {
                             NSArray *a = [ [ components objectAtIndex:
2 ] componentsSeparatedByString: @":" ];
                             if ([ a count ] >= 2) {
                                   alert.missingDate = [ NSDate
dateWithNaturalLanguageString: [ [ [ a objectAtIndex: 1 ]
stringByReplacingOccurrencesOfString: @"." withString: @"" ]
stringByReplacingOccurrencesOfString: @" " withString: @"" ] ];

                             }
                        }

                        NSArray *array = [ alert.summary
componentsSeparatedByString: @"," ];
                        if ([ array count ] >= 4) {
                             NSArray *loc = [ [ array objectAtIndex: 2 ]
componentsSeparatedByString: @"." ];
                             NSString *l = nil;
                             if ([ loc count ] > 1)
                                   l = [ [ loc objectAtIndex: 1 ]
stringByTrimmingCharactersInSet: [ NSCharacterSet whitespaceCharacterSet ] ];
                             NSString *st = [ [ [ [ array objectAtIndex: 3
 ] componentsSeparatedByString: @"." ] objectAtIndex: 0 ]
stringByTrimmingCharactersInSet: [ NSCharacterSet whitespaceCharacterSet ] ] ;

                             alert.summary = [ NSString stringWithFormat:
@"%@\n%@, %@", [ [ array objectAtIndex: 1 ] stringByTrimmingCharactersInSet:
 [ NSCharacterSet whitespaceCharacterSet ] ], l, st];
                        } else {
                             alert.summary = @"";
                        }

                   } else if ([ line hasPrefix: @"<enclosure" ]) {
                        victim.photourl = [ [ [ [ line
componentsSeparatedByString: @" " ] objectAtIndex: 3 ]
componentsSeparatedByString: @"\"" ] objectAtIndex: 1 ];
                   }
                   i++;
                   line = [ xmlBuffer objectAtIndex: i ];
             }

             if (alert != nil) {
                   NSDate *aDate, *alertDate;
                   alertDate = (alert.missingDate == nil) ?
alert.issueDate : alert.missingDate;

                   int j;
                   for(j = 0; j < [ activeAlerts count ]; j ++) {
                        MissingKidAlert *a = [ activeAlerts objectAtIndex: j ];
                        aDate = (a.missingDate == nil) ? a.issueDate : a.missingDate;
                        if ([ alertDate isEqual: aDate ])
                             continue;
                        if ([ alertDate earlierDate: aDate ] != alertDate) {
                             break;
                        }
                   }
                   if (alert != nil && j >= 0)
                        [ activeAlerts insertObject: alert atIndex: j ];
             }
        }
  }

  NSLog(@"%s total active alerts: %d", __func__, [ activeAlerts count ]);

  [ alertsTable reloadData ];
  waitView.hidden = YES;
  [ self.view addSubview: alertsTable ];
  [ activityIndicator stopAnimating ];
  if (nConnections == 0)
        [ UIApplication sharedApplication ].networkActivityIndicatorVisible = NO;
  [ receivedData release ];
  [ self loadVisibleCells ];
}

- (void)dealloc {

  for (MissingKidAlert *alert in activeAlerts)
        [ alert release ];
  [ activeAlerts removeAllObjects ];
  [ activeAlerts release ];
  [ alertsTable release ];
  [ super dealloc ];
}

- (MissingKidAlert *)findConnection:(NSURLConnection *)connection {
  for (MissingKidAlert *alert in activeAlerts) {
        if (alert.connection == connection)
             return alert;
  }
  return nil;
}

- (int)findIndex:(NSURLConnection *)connection {
  int i = 0;
  for (MissingKidAlert *alert in activeAlerts) {
        if (alert.connection == connection)
             return i;
        i++;
  }
  return −1;
}

/* NSURLConnection methods */

-(void)connection:(NSURLConnection *)connection didReceiveResponse:
(NSURLResponse *)response {
  if (connection == alertsConnection) {
        receivedData = [ [ NSMutableData data ] retain ];
  } else {
        MissingKidAlert *alert = [ self findConnection: connection ];
        if (alert != nil)
             [ alert.data setLength: 0 ];
  }
}

-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{

  if (connection == alertsConnection) {
        [ receivedData appendData: data ];
  } else {
        MissingKidAlert *alert = [ self findConnection: connection ];
        if (alert != nil)
             [ alert.data appendData: data ];
  }
}


-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
  if (connection == alertsConnection && connection != nil) {

        NSLog(@"%s query complete", __func__);

        [ connection release ];

        alertsConnection = nil;
        if ([ receivedData length ] == 0) {
             fail = YES;
             UIAlertView *alertView = [ [ UIAlertView alloc ]
initWithTitle: @"Connection Error" message: @"Received an invalid
response from the server" delegate:
 self cancelButtonTitle: nil otherButtonTitles: @"OK", nil ];
             [ alertView show ];
             [ receivedData release ];
        } else {
             [ self parseXML ];
        }
  } else {
        MissingKidAlert *alert = [ self findConnection: connection ];
        int idx = [ self findIndex: connection ];
        if (alert != nil) {
             alert.victim.photo = [ [ UIImage alloc ] initWithData: alert.data ];

             if (idx >= 0) {
                   MissingKidsAlertTableViewCell *cell =
(MissingKidsAlertTableViewCell *) [ alertsTable cellForRowAtIndexPath:
 [ [ NSIndexPath indexPathWithIndex:
 0 ] indexPathByAddingIndex: idx ] ];
                   if (cell != nil)
                        cell.image = alert.victim.photo;
             }
        }
        nConnections--;
        if (nConnections == 0)
             [ UIApplication sharedApplication ].networkActivityIndicatorVisible = NO;
        [ connection release ];
        [ alert.data release ];
        alert.data = nil;
        alert.connection = nil;
  }
}

-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
  if (connection == alertsConnection) {
        NSLog(@"%s query failed with error: %@", __func__, [
error localizedDescription ]);

        [ UIApplication sharedApplication ].networkActivityIndicatorVisible = NO;

        waitView.hidden = YES;
        alertsTable.alpha = 1.0;
        [ activityIndicator stopAnimating ];
        [ alertsTable reloadData ];

        fail = YES;
        UIAlertView *alertView = [ [ UIAlertView alloc ] initWithTitle:
 @"Connection Error" message: [ [ error localizedDescription ] capitalizedString ]
delegate: self cancelButtonTitle: nil otherButtonTitles: @"OK", nil ];
        [ alertView show ];
        [ connection release ];
        [ receivedData release ];
  } else {
        MissingKidAlert *alert = [ self findConnection: connection ];
        int idx = [ self findIndex: connection ];

        if (alert != nil) {
             alert.connection = nil;
             [ alert.data release ];

             MissingKidPerson *victim = alert.victim;

             NSLog(@"%s photo failed to load for %@ %@", __func__,
victim.firstName, victim.lastName);

             victim.photo = nil;
             if (idx >= 0) {
                   MissingKidsAlertTableViewCell *cell =
(MissingKidsAlertTableViewCell *) [ alertsTable cellForRowAtIndexPath: [ [
NSIndexPath indexPathWithIndex: 0 ] indexPathByAddingIndex: idx ] ];
                   if (cell != nil)
                        cell.image = nil;
             }
        }
        nConnections--;
        if (nConnections == 0)
             [ UIApplication sharedApplication ].networkActivityIndicatorVisible = NO;
        [ connection release ];
  }
}

/* UIAlertViewDelegate methods */

- (void)alertView:(UIAlertView *)alertView
didDismissWithButtonIndex:(NSInteger)buttonIndex {

  [ alertView release ];
}

/* UITableViewDelegate methods */

- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {

  return 81.0;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
(NSInteger)section {
    return [ activeAlerts count ];
}

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";
    MissingKidsAlertTableViewCell *cell =
(MissingKidsAlertTableViewCell *) [tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
  MissingKidAlert *alert = [ activeAlerts objectAtIndex: [
indexPath indexAtPosition: 1 ] ];

    if (cell == nil) {
        cell = [ [ [ MissingKidsAlertTableViewCell alloc ]
initWithFrame: CGRectZero reuseIdentifier: CellIdentifier ] autorelease ];
        cell.accessoryType = UITableViewCellAccessoryNone;
        cell.issueDate.textColor = [ UIColor redColor ];
  }

  cell.name.text = [ [ NSString stringWithFormat: @"%@ %@", alert.
victim.firstName, alert.victim.lastName ] capitalizedString ];
  cell.issueDate.text = [ NSString stringWithFormat: @"%@ %@",
                                (alert.missingDate == nil) ?
@"Alert Issued" : @"Missing Since",
                                [ ((alert.missingDate == nil) ?
alert.issueDate : alert.missingDate)
descriptionWithCalendarFormat:@"%m-%d-%Y"


timeZone: nil


                  locale: [[NSUserDefaults standardUserDefaults]
dictionaryRepresentation] ]
                                ];

  cell.image = alert.victim.photo;
  cell.description.text = alert.summary;
  cell.accessoryType = UITableViewCellAccessoryNone;
  cell.selectionStyle = UITableViewCellSelectionStyleNone;

    return cell;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:
(BOOL)decelerate {
  if (decelerate == NO)
        [ self loadVisibleCells ];
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  [ self loadVisibleCells ];
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
  [ self loadVisibleCells ];
}

@end

Example 7.8. Table Cell Layout Headers (MissingKidsAlertTableViewCell.h)

#import <Foundation/Foundation.h>


@interface MissingKidsAlertTableViewCell : UITableViewCell {
  UIImageView *thumbnail;
  UILabel *name, *issueDate, *location, *description;
}

- (void)setImage:(UIImage *)image;
@property(assign) UILabel *name, *issueDate, *location, *description;

@end

Example 7.9. Table Cell Layout Class (MissingKidsAlertTableViewCell.m)

#import "MissingKidsAlertTableViewCell.h"

@implementation MissingKidsAlertTableViewCell
@synthesize name, issueDate, location, description;

- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
  self = [ super initWithFrame: frame reuseIdentifier: reuseIdentifier ];
  if (self != nil) {
        name = [ [ UILabel alloc ] initWithFrame: CGRectMake(75.0,
5.0, 205.0, 15.0) ];
        name.text = @"Name Unknown";
        name.font = [ UIFont boldSystemFontOfSize: 12.0 ];
        name.textColor = [ UIColor blackColor ];
        name.backgroundColor = [ UIColor clearColor ];
        name.adjustsFontSizeToFitWidth = YES;

        description = [ [ UILabel alloc ] initWithFrame: CGRectMake(75.0,
18.0, 205.0, 28.0) ];
        description.font = [ UIFont systemFontOfSize: 11.0 ];
        description.textColor = [ UIColor blackColor ];
        description.backgroundColor = [ UIColor clearColor ];
        description.adjustsFontSizeToFitWidth = YES;
        description.numberOfLines = 2;
        description.lineBreakMode = UILineBreakModeWordWrap;

        issueDate = [ [ UILabel alloc ] initWithFrame: CGRectMake(75.0,
58.0, 205.0, 13.0) ];
        issueDate.font = [ UIFont systemFontOfSize: 11.0 ];
        issueDate.textColor = [ UIColor redColor ];
        issueDate.backgroundColor = [ UIColor clearColor ];
        issueDate.adjustsFontSizeToFitWidth = YES;

        thumbnail = [ [ UIImageView alloc ] initWithImage: [ UIImage
imageNamed: @"Thumbnail_Small.png" ] ];
        thumbnail.frame = CGRectMake(0.0, 0.0, 64, 80.0);
        thumbnail.contentMode = UIViewContentModeScaleAspectFill;
        thumbnail.clipsToBounds = YES;

        [ self setImage: nil ];
  }
  return self;
}

- (void)layoutSubviews {

  [ thumbnail removeFromSuperview ];
  [ name removeFromSuperview ];
  [ issueDate removeFromSuperview ];
  [ description removeFromSuperview ];

  [ super layoutSubviews ];

  [ self addSubview: thumbnail ];
  [ self addSubview: name ];
  [ self addSubview: issueDate ];
  [ self addSubview: description ];
}

- (void)setImage:(UIImage *)image {
  if (image == nil)
        thumbnail.image = [ UIImage imageNamed: @"Thumbnail_Small.png" ];
  else
        thumbnail.image = image;
}

- (void)dealloc {
  [ name release ];
  [ issueDate release ];
  [ thumbnail release ];
  [ super dealloc ];
}

@end

If you enjoyed this excerpt, buy a copy of iPhone SDK Application Development.

Copyright © 2009 O'Reilly Media, Inc.