Networking



Relevant resources


Documentation


Web


Books


Sample code - Apple


Sample code - class


Sample code - other


Networking is a critical part of the iPhone experience.  The iPhone's always-on EDGE or 3G networking, or even the iPod touch's WiFi networking, make these devices capable of pulling data from and sending data to a vast number of sources, from Twitter streams to seismology center readouts.  There are a number of ways to send and receive data over the various network types that the iPhone supports, some fairly low-level, but most abstract away the underlying details of the network from you.


Custom URL schemes


Before we start getting too far into interacting with networked machines, there is an interesting capability of iPhone applications that should be pointed out.  iPhone applications exist within their own sandboxes, so the options for communicating between applications or causing an application to be launched on a certain event are limited.  Custom URL schemes provide one way of doing this.


Your application can register a custom URL scheme (myapplication://) that will launch your application and have the URL be fed into the application at startup.  This lets other applications start yours, with arbitrary string data being fed in for the remainder of the URL.  This string data can be anything, including base64-encoded data.


The first step in registering a custom URL scheme is to edit your Info.plist to indicate the name of the custom URL scheme you will be registering.  For example, Molecules registers the molecules:// URL scheme with the following:


<key>CFBundleURLTypes</key>

<array>

<dict>

<key>CFBundleURLName</key>

<string>com.sunsetlakesoftware.Molecules</string>

<key>CFBundleURLSchemes</key>

<array>

<string>molecules</string>

</array>

</dict>

</array>


There are two places that you can handle custom URL schemes, both delegate methods that would reside within your application delegate:


- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions


Apple recommends handling the custom URL within the latter method, using that instead of applicationDidFinishLaunching:.  You can retrieve the URL and the application that triggered it by querying the UIApplicationLaunchOptionsURLKey and UIApplicationLaunchOptionsSourceApplicationKey in the launchOptions dictionary.


You can also use custom URL schemes to open other applications.  There is a rudimentary wiki for custom URL schemes, if you want to look up those that have been registered for more common applications.  The following code checks to see if an installed application can handle the custom URL, then passes it off to the system:


NSURL *customLaunchURL = [[NSURL alloc] initWithString:[NSString stringWithFormat:@"customapplication://%@", stringToSend]];

if ([[UIApplication sharedApplication] canOpenURL:customLaunchURL])

{

[[UIApplication sharedApplication] openURL:customLaunchURL];

}


This will exit your application and start the other with this custom URL.


Apple's built-in applications have custom URL schemes of their own.  These include:



NSURLConnection


The easiest way to retrieve data from a particular URL is through an NSURLConnection.  NSURLConnection provides an asynchronous means of downloading data, and uses callbacks to incrementally feed in downloaded data and any status updates regarding the connection.  All of the low-level network connection details are abstracted away from you if you use this class, so you don't need to worry about what network this is going over (EDGE, 3G, WiFi, etc.).


To create an NSURLConnection, you can use code like the following:


downloadFileName = [[locationOfRemoteFile lastPathComponent] retain];

NSURLRequest *theRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:locationOfRemoteFile] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];

NSURLConnection *downloadConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];


where locationOfRemoteFile is an NSString containing a full URL to a particular location.


Before we create an NSURLConnection, we must first set up an NSURLRequest to be used to initiate the connection.  In this case, we generate an NSURL from the NSString we started with, set the caching policy for the request (in this case, the default for the protocol used), as well as a timeout for the connection (60 seconds).  This is a simple request.  For a more complex case where you need to POST data with the request, you can use code like the following:


NSString *webServicesURL = @"http://www.pdb.org/pdb/services/pdbws";

NSMutableURLRequest *theRequest=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:webServicesURL] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];

theRequest.HTTPMethod = @"POST";

[theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-Type"];

[theRequest setValue:@"" forHTTPHeaderField:@"Soapaction"];

[theRequest setValue:@"www.pdb.org" forHTTPHeaderField:@"Host"];

NSString *pdbQueryBody = [NSString stringWithFormat:@"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope\nxmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\nxmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\"\nSOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"\nxmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"> <SOAP-ENV:Body>\n<m:keywordQuery xmlns:m=\"webservices.pdb.org\">\n<in0 xsi:type=\"xsd:string\">%@</in0>\n<in1 xsi:type=\"xsd:boolean\">false</in1>\n<in2 xsi:type=\"xsd:boolean\">false</in2>\n</m:keywordQuery>\n</SOAP-ENV:Body>\n</SOAP-ENV:Envelope>", keyword];

theRequest.HTTPBody = [pdbQueryBody dataUsingEncoding:NSUTF8StringEncoding];


In this more complex example, we are posting data using the SOAP interface for the RCSB Protein Data Bank, so we need to set a few of the fields manually within the URL request.  An NSMutableURLRequest is created instead of the static NSURLRequest used previously.


Once an NSURLConnection has been initiated, it will begin calling back to its delegate as the connection progresses.  First, the delegate should receive a callback on 


- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;


which will indicate that the remote machine has responded to the initial request.  The NSURLResponse you get back has interesting information within it, including



- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;


will be called, with an NSError instance that contains exact details about what failed.  Almost always, the failure will be a dropped or nonexistant network connection.


As data comes in, the following method will be triggered:


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


The NSData passed in contains the raw binary data downloaded since the last time the method was called.  Usually, you will append this data to a running NSMutableData instance that you have going, but if the file becomes too large you may want to look toward incrementally writing this data to disk so that you do not exhaust available memory.


Finally, 


- (void)connectionDidFinishLoading:(NSURLConnection *)connection;


will be called when the connection has successfully completed.  It is in this method that you usually start processing the input or writing it to disk.


Network status indicator


As a nice UI touch, you can activate the network status indicator in the top status bar of the iPhone while performing a network operation using

[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];


and deactivate it using 


[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];


Reachability


In a network-reliant application, you may wish to keep track of the status of your network connection.  Anticipating this need, Apple has provided the Reachability example that demonstrates the use of the SCNetworkReachability API for providing asynchronous checks and callbacks that let you know when a path to a particular network destination is unreachable.


One warning they provide in the documentation to this sample is worth reading:


The Reachability sample demonstrates the asynchronous use of the

SCNetworkReachability API. You can use the API synchronously, but do not

issue a synchronous check by hostName on the main thread. If the device

cannot reach a DNS server or is on a slow network, a synchronous call to

the SCNetworkReachabilityGetFlags function can block for up to 30

seconds trying to resolve the hostName. If this happens on the main

thread, the application watchdog will kill the application after 20

seconds of inactivity.


Displaying web content


Rendering web content is one of the most complex tasks a modern application can do.  Fortunately, Apple has made it easy to embed the same WebKit rendering engine employed by the iPhone's Safari implementation within your application.  This makes it trivial to create your own web browser, or even an iPhone application built entirely around one website.


The web rendering is provided via UIWebView.  Once you have created a UIWebView instance somewhere, you can have it start loading the page at a URL by creating an appropriate request in the way we did for an NSURLConnection:


NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];

[webView loadRequest:theRequest];


If you set your controller to be a delegate for the web view, you will be able to get a series of callbacks during the loading process within the web view:


- (void)webViewDidStartLoad:(UIWebView *)webView

- (void)webViewDidFinishLoad:(UIWebView *)thewebView


These are triggered when the web view starts and stops loading content, and are good places to activate and deactivate a spinning activity indicator.


- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error


This lets you handle any connection-related errors.  Again, most of these will be due to network timeouts due to poor or missing network connections.


- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType


This method actually is called before any loading takes place within the web view, and gives you the option of doing something in response to links being clicked within the view.  For example, you may detect the click event for a file type specific to your application, and download it for display in a separate view rather than show it in the web view.  If you return NO from this method, the web view will not follow the URL provided to it by clicking a link or another source.


You can direct the navigation of a UIWebView using the methods stopLoading, reload, goBack, and goForward.  UIWebView also provides the canGoBack and canGoForward properties, which can be used to grey out or hide directional buttons that might not do anything based on the current navigational state of the web view.


When you set up your web view, you may wish to set the scalesPageToFit property to YES to provide a more aesthetically pleasing experience.


UIWebViews are great not just for rendering the external Web, but for display of complex local content.  One example of this is an in-application user manual.  You can create a complex, formatted manual from HTML and CSS, and then bundle it with your application so it can be accessed anywhere, not just over the network.  I did this in one of my applications, and my users really took to it.


You can lay out your HTML, CSS, and images in a local subdirectory within your project directory, then drag that directory into your Xcode project.  If you choose the option Create Folder References for any added folders the directory will be added as a reference, and its directory structure will be preserved within your application bundle.  As you add or remove files in this directory, they will also be added or removed from your final product.  You can also achieve something similar by creating a custom Copy Files build phase, set the destination to Resources, and give a custom directory name.  If you drag all of your help resources into that build phase, they will all end up there in your final application.


To open the local HTML index file, you can use code like the following:


NSString *path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Help"];  

NSURL *helpURL = [NSURL fileURLWithPath:[path stringByAppendingPathComponent:@"helpintroduction.html"]];

NSURLRequest *URLReq = [NSURLRequest requestWithURL:helpURL];

[webHelpView loadRequest:URLReq];


If you use relative, rather than absolute, references within your HTML, everything should load from within your application bundle as if it were running on a remote server.


UIWebViews can also be used to provide cheap and easy viewing of all of the file types supported by the iPhone, including:


Excel (.xls)

Keynote (.key.zip)

Numbers (.numbers.zip)

Pages (.pages.zip)

PDF (.pdf)

Powerpoint (.ppt)

Word (.doc)


See the tech note Using UIWebView to display select document types for more on this.


Finally, you can even use a UIWebView as a JavaScript interpreter using the stringByEvaluatingJavaScriptFromString: method, which is self-explanatory.


Composing and sending email


iPhone OS 3.0 added a commonly requested feature: the ability to compose and send emails from within your application, including adding attachments.  This is done through MFMailComposeViewController, which provides a full mail composer view that lets your users compose and send email as if they were in the native mail client.


Before trying to show an MFMailComposeViewController, you will want to check if the user's device is configured to send email using code like the following:


if ([MFMailComposeViewController canSendMail])


You may wish to gray out the option to send email if this returns a false value.


To configure an MFMailComposeViewController, you can use code like the following:


mailViewController = [[MFMailComposeViewController alloc] init];

mailViewController.mailComposeDelegate = self;

[mailViewController setSubject:emailSubject];

if (emailBody != nil)

[mailViewController setMessageBody:emailBody isHTML:NO];

if (emailAttachment != nil)

[mailViewController addAttachmentData:emailAttachment mimeType:@"application/pdf" fileName:@"Document.pdf"];


This pre-populates the MFMailComposeViewController with a subject, as well as body text and an attachment, if they exist.  Note that the body text can use HTML with the appropriate setting.  Attachments are added using NSData instances that contain their raw data, then specifying the MIME type of the file and the filename to use.


To display the MFMailComposeViewController, you can use -presentModalViewController:animated: to slide it up onto the screen, or just add the controller's view to some other display element you would like to present.


To handle dismissal of the controller, you will need to make a controller object the mailComposeDelegate of the MFMailComposeViewController.  This delegate will need to conform to the MFMailComposeViewControllerDelegate protocol and handle the delegate method


- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error


You'll place your code to dismiss the MFMailComposeViewController from view, and possibly handle the result of the mail editing operation (MFMailComposeResultCancelled, MFMailComposeResultSaved, MFMailComposeResultSent, or MFMailComposeResultFailed).


Game Kit


In iPhone OS 3.0, Apple introduced Game Kit, which makes it trivial to implement Bluetooth-based peer-to-peer networking between iPhone OS devices.  Game Kit also lets you implement voice communication between two iPhone users.  Unfortunately, Game Kit does not work with first-generation iPhones and iPod touches, and does not let you network with Macs over Bluetooth either.


In iPhone OS 3.1, Apple extended Game Kit to support device connection over WiFi.  This means that you can also network between the Simulator and the iPhone using Game Kit in OS 3.1.


One thing to be aware of within the iPhone is that it shares the same radio antenna between WiFi and Bluetooth, so communications on one can disrupt or slow the other.  


Game Kit uses Bonjour for discovery of other devices (which we'll talk about in a bit) over Bluetooth or WiFi (in OS 3.1) networks, and sockets to connect and send data between devices.  All of this networking code is abstracted away from you, requiring you to implement only a few simple things.


To use Game Kit within your application, you will need to include the proper header:


#import <GameKit/GameKit.h>


and make sure that your application is linking against the GameKit framework.


A connection between devices is managed by a GKSession, which you can create using code like the following:


GKSession *session = [[GKSession alloc] initWithSessionID:@"edu.matcmadison.gamekitexample" displayName:nil sessionMode:GKSessionModePeer]; 


This creates a session that is identified using the string edu.matcmadison.gamekitexample.  This identifier lets the GKSession pick out instances of this application from other applications.  The displayName is a string that identifies this particular device's session.  If you leave that as nil, the iPhone's name will be used for this.  Finally, we are setting up this session as a peer-to-peer connection using GKSessionModePeer.  Other session types include GKSessionModeServer and GKSessionModeClient, for starting the session as a server or client, respectively.


When you want a GKSession to begin advertising itself as being available for a connection, you set its available property to YES.


A GKSession uses a delegate conforming to the GKSessionDelegate protocol to manage the connection to peers.  The delegate protocol methods are 


- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error

- (void)session:(GKSession *)session didFailWithError:(NSError *)error


These two let you handle cases where there were problems establishing or maintaining a connection with a peer.


- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID


This will be triggered when a peer has requested a connection to this device.  This connection request is initiated by code like


[session connectToPeer:peerID withTimeout:0];


and can be responded to using 


[session acceptConnectionFromPeer:peerID error:&error];


Finally. the delegate method


- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state


Provides notification of when a change in state has occurred for another peer using your application.  If the state is GKPeerStateAvailable, you might want to initiate a connection with that peer using the code described above.  Other states include GKPeerStateUnavailable, GKPeerStateConnected, GKPeerStateDisconnected, and GKPeerStateConnecting.


You can manage the user interface for displaying available peers, choosing one, and responding to a connection request, but Game Kit provides a slick interface for doing this for you in the GKPeerPickerController.  The GKPeerPickerController will handle much of the initial connection work for you.  You can create and show the peer picker using code like the following:


GKPeerPickerController *picker = [[GKPeerPickerController alloc] init];

picker.delegate = self;

[picker show];


The delegate for the GKPeerPickerController must conform to the GKPeerPickerControllerDelegate protocol, and provide the following delegate methods:


- (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type


This lets you construct and return a GKSession for use by the peer picker controller.


- (void)peerPickerController:(GKPeerPickerController *)picker didSelectConnectionType:(GKPeerPickerConnectionType)type


By default, the peer picker assumes that you only want to connect to other local devices using Bluetooth (or WiFi in 3.1).  However, you can make it prompt the user with a choice of either connecting locally or going out to the Internet to find other peers using code like the following


picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby | GKPeerPickerConnectionTypeOnline;


during the setup of the peer picker.  The previous delegate method lets you know what type of network connection the user selected, so that you could use your own networking code for non-local connections.


- (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerIDtoSession:(GKSession *)session


This is triggered when a connection is made between this device and a peer.  You can now setup the rest of the session details and start communicating between the peers.


- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker


If the user cancels the peer picker at any time, this method is triggered.Networking


To send and receive data, you'll need to first set up a handler for the data to be received.  This can be done using code like the following:


[session setDataReceiveHandler:self withContext:NULL];


The data handler must implement a method with the following signature:


- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context;


which will deal with received data from other peers.


To send data to a specific peer, you can use code like


[session sendData:packet toPeers:[NSArray arrayWithObject:gamePeerId] withDataMode:GKSendDataReliable error:nil];


where packet is an NSData instance containing whatever data packet you want to send between peers.  An array of recipients is specified (only one in this case), along with the data transfer mode.  The possible values for transfer modes are GKSendDataReliable, where data packets are guaranteed to arrive at the destination in the order they were sent, and GKSendDataUnreliable, where data packets may or may not make it to their destination, but they get there fast.


When done with a session, you'll want to close out the GKSession using code like the following:


[session disconnectFromAllPeers];

session.available = NO;

[session setDataReceiveHandler: nil withContext: nil];

session.delegate = nil;


In addition to direct networking, Game Kit provides voice chat functionality (including audio processing like echo suppression) using GKVoiceChatService.


Bonjour


As mentioned above, Bonjour is used by Game Kit to perform discovery and connection to other devices.  Bonjour is referred to on other platforms as ZeroConf, for zero-configuration networking.  The stated goal with Bonjour is to make network connections as easy to work with as USB is.


Bonjour abstracts away IP addresses, ports, DNS resolution, and all the other lower-level details for finding other network devices, making it simple to connect to other devices over WiFi and Bluetooth (as of iPhone OS 3.0).


Bonjour can be managed through the NSNetServices Cocoa API or the lower-level Core Foundation CFNetServices API.  I'll only talk about the Cocoa API here, but if you're interested in lower-level networking, I suggest reading the NSNetServices and CFNetServices Programming Guide and the CFNetwork Programming Guide.


A single instance of a Bonjour service is represented by an instance of NSNetService.  If your application is acting as a server, it will publish an NSNetService.  If it is a client, it will use NSNetService instances to represent remote services.


NSNetServices can be discovered using an NSNetServiceBrowser, which will scan for services of a particular type.  This searching can be kicked off by creating an instance of NSNetServiceBrowser and using code like


[netServiceBrowser searchForServicesOfType:type inDomain:domain];


where type has the structure _applicationprotocol._transportprotocol.  Currently, only _tcp or _udp are supported for the _transportprotocol portion.  The application protocol should be unique to your application, containing lower-case letters, digits, and hyphens, begin and end with a lower-case letter or digit, and be a max of 14 characters.  You may wish to guarantee that you are using a unique name for your Bonjour service.  Services can be registered at http://www.dns-sd.org/ServiceTypes.html , which also is a great place to check and make sure that your service name isn't in use by some other application.


The domain will typically be @"local".


It will run this search asynchronously and report back services using the following delegate methods:


- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didFindService:(NSNetService *)service moreComing:(BOOL)moreComing 

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing 


These let you update a table or other list of available remote devices the user can choose from.


Once the decision has been made as far as what device to connect to, the IP address and other parameters for the named service need to be resolved.  To start the resolving process, you use code like the following:


[currentlyResolvingService resolveWithTimeout:0.0];


This will start the process of resolving the service with no timeout, which might be necessary on the high-latency Bluetooth or WiFi connections you'll encounter on the iPhone.  The resolving NSNetService will update its condition via the delegation methods


- (void)netServiceDidResolveAddress:(NSNetService *)service 

- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict


The former reports back when a service has been resolved, allowing you to connect and start sending data.  The latter is triggered on a timeout, which will never happen in the example above.


When the service has been resolved, you can get input and output streams associated with the service using 


NSInputStream *inputStream = nil;

NSOutputStream *outputStream = nil;

    

if([currentlyResolvingService getInputStream:&inputStream outputStream:&outputStream]) 

{

    self.inputStream = inputStream;

    self.inputStream.delegate = self;

    [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]

                                forMode:NSDefaultRunLoopMode];

    [self.inputStream open];

    

    self.outputStream = outputStream;

    self.outputStream.delegate = self;

    [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]

                                 forMode:NSDefaultRunLoopMode];

    [self.outputStream open];

}


For more on streams and how to manage data transfer with them, I refer you to the Stream Programming Guide for Cocoa.


CFNetwork


As I mentioned, if you need lower-level network access on the iPhone, the CFNetwork API is available to you and is described in the CFNetwork Programming Guide.  You can go lower than that, but Apple provides this caveat:


Although BSD (POSIX) networking APIs are available in iPhone OS, you should avoid using them. If you communicate directly with sockets, certain networking capabilities of iPhone OS, such as VPN On Demand, do not work.