Network communications from an iOS device are easy, but handling errors on those connections is not. As an app developer I like to see results quickly and leave the edge condition details till later so I quickly wire in network operations and plan on handling all of the error conditions later. For web applications I can usually get away with this approach because the network connectivity from a stationary laptop is mostly reliable. In a mobile app this approach will get me in trouble. When I do get around to adding exception handling routines I end up in a situation where I need to refactor my network code significantly or hack in less than optimal approaches to handle exceptions.
This article proposes a design pattern that both provides for reasonably quick results from your coding efforts and creates an elegant and robust framework for exception conditions.
In mobile development we see three major exception types for mobile communications.
- Server not reachable due to no network connectivity to the device.
- Server returns some error code because of a failure on the server (or limited connectivity – the request times out).
- The user needs to authenticate and either the client code or server code has detected an unauthenticated request.
As the number of potential exception conditions increase linearly, the amount of code required to handle them increases exponentially. This pattern attempts to bend that exponential curve back to a more linear curve.
The approach suggested uses a command dispatch pattern combined with a broadcast notification pattern.
The pattern consists of the following object types: controllers, command objects, exception listeners, and command queues. The sections below describe the behavior at a high level of each type of object.
- Controllers: These are typically view controllers, that request data and processes the results. In this design pattern the controllers do not need to contain any exception handling logic. The only cases it needs to handle is a successful completion or a completely unretryable failure of the service. In the utter failure scenario the controller would typically pop itself off the view stack because the user has already been informed of the failure by the exception listener objects described below. Controllers create commands and listen for the commands completion.
- Command object: Command objects correlate to the different network transactions that the application will perform. You may have a command object to retrieve images, fetch JSON data from an specific REST endpoint, or POST information to a service. In iOS, a command object is a subclass of NSOperation. Much of the logic of a command object is common to other command objects so a superclass command object to handle common logic is recommended. A command object will have the following attributes:
- Completion notification name. In iOS, controllers register themselves as observers for this notification name. When the service call returns successfully the command object uses NSNotificationCenter to broadcast a notification with this name. This name is usually unique to the command class.
- Server error exception name. A special exception handler objects listen for this notification. The command object will use NSNotificationCenter to broadcast a message with this name when the server times out our returns an error not related to authentication. All command classes usually share the same exception names thereby sharing the same exception listeners.
- Reachability exception name. The command object produces a notification of this type when it detects a lack of reachability to the target server. Another exception listener may listen for this type of exception. In some apps this type of exception is not needed because the reachability exceptions are handled by the server error exception listener.
- Authentication exception name. The command object may produce a notification of this type if it determines the user is not authenticated or the server reports an unauthenticated status. A third exception listener waits for this type of notification to appear. The exception notification names are typically shared across all notifications in the app.
- Custom attributes specific to the request being made. These values are typically supplied by the issuing controller. These are usually parameters needed for the specific service call. These attributes are the business data for the service call and will be as varied as the business problems being solved.
- Exception listener. The exception listeners are typically instantiated by the app delegate and remain in the background waiting for a specific type of notification. In many cases the exception listener will display a modal view controller when it receives a notification. I'll describe their behavior later.
- Command queue. Controllers submit commands to the command queue for processing. In iOS command queues are NSOperationQueue objects. An app may have one or more command queues. I recommend against using the main queue as a command queue since that will impair the user experience by executing long running operations on the main thread. Using NSOperationQueues provides built-in capability for managing the number of active operations and interrelationships between operations.
Each of these objects has it's own part to play in successfully completing a network transaction. The following section describes their respective roles in this dance.
Controllers are focused on executing UI and business logic. When a controller wants data from a service it should takes the following actions:
- Creates a network command object
- Initializes the specific attributes of the command object
- Registers as an observer for the completion of the command
- Pushes the command onto an operation queue for execution
- Waits for the NSNotificationCenter to deliver a completion notification
When the operation completes the controller receives a completion notification and may take the following actions:
- Check the status of the operation to see if it was successful.
- If successful it processes the received data. The received data is supplied to the controller via the userinfo attribute of the NSNotification object. Keep in mind that the notification will probably arrive on a thread other than the main thread. If the controller is manipulating the UI then it will need to dispatch that code block on the main thread, usually via a Grand Central dispatch.
- If unsuccessful the controller has a number of options depending on the app requirements. It may pop itself off the view stack, or display information indicating that the data is not available. At this point it should not need to ask to retry or raise an alert notification, that was probably already done by an exception listener involved in this pattern.
- The controller may remove itself as an observer for the commands completion notification. In some cases this is not desirable if the controller wants to monitor for other data arriving from the server that originates from this command type.
Notice that controllers do not have any logic to handle retries, timeouts, authentication, or reachability, that will all be done by the commands and exception listeners. If the controller wishes to guarantee that only it will receive the returned data then the controller can alter the completion notification name for that particular command object instance to be a unique value and listen for notifications of that unique name.
Command objects are focused on calling the target service and broadcasting the results of that service call. The steps generally taken by a command object are:
- Check for reachability. If the network is not reachable then broadcast a reachability exception notification.
- Check for authentication status if required. If the user is not yet authenticated it broadcasts an authentication exception notification.
- Build the network request using the custom properties provided by the controller. Usually the endpoint URL is specified as a static attribute of the command object class.
- Issue the network request using a synchronous request.
- Check the status of the request. If the status is a server error then it broadcasts a server exception notification. If the error is an authentication error then it broadcasts an authentication exception notification.
- Parse the results.
- Broadcast a completion notification with a successful status.
When a command object broadcasts a notification, completion or otherwise, it needs to create a dictionary object that contains a copy of itself, the status of the call, and any data returned as a result of the call. The copy of itself is necessary because an NSOperation can only be submitted once. As you'll see in the exception listeners this command may get resubmitted when the exception is handled by the listener.
The synchronous request API is ideally suited to this pattern because the commands are executed off of the main thread. If the request returns a larger amount of data than you wish to squeeze into memory then you'll need to move away from synchronous requests to asynchronous requests. Because the main function of an NSOperation is a single method, one must implement concurrency locking to have the main method block until the asynchronous call completes.
Exception listeners are the magic pixie dust that makes this pattern especially powerful. These objects are usually created by the app delegate and remain in the background listening for notifications. When a notification is received it is the responsibility of the listener to inform the user and potentially solicit the users desired response (other than throwing the phone through a wall). In the exception notification there is a copy of the command that triggered the exception. Once the user has responded the listener will usually resubmit the command back onto the queue to be retried. One interesting caveat for the exception listeners is that since multiple commands may be in-flight there may be multiple exception notifications generated while the user is still responding to the first exception. Because of this, the exception listeners must collect exception notifications and resubmit all of the triggering commands when the first exception is handled.
The flow for a server exception could be:
- Present a nice looking modal dialog explaining the error and giving the user the option to cancel or retry.
- Collect any other server exceptions that may be broadcast.
- If the user selects retry then the dialog dismisses and then the listener resubmits all of the collected commands.
- If the user selects cancel then the dialog dismisses and the listener sets the command completion status to failed for all of the collected commands and asks each command to broadcast a completion notification.
The flow for a reachability exception may be:
- Present a nice looking modal dialog informing the use they need to be on a network.
- Collect any other service exceptions that may be broadcast
- Listen for reachability changes and when the network is reachable dismiss the dialog and resubmit the command.
The flow for an authentication exception is a bit more complicated. Keep in mind that commands are independent of one another and many can be in flight at any one time. The authentication flow uses command types specific to authenticating. Because of this the flow leverages the same error and reachability capabilities of normal operations but without the authentication checks. The flow may look like:
- Present a login view as a modal view controller.
- Continue collecting commands that failed due to authentication errors.
- Receive input from the user.
- If the user cancels the login the listener should send a completion notification for the triggering commands with a failure status.
- Create a login command and place it on a queue.
- Wait for the main loop for the receipt of a completion notification.
- If the login didn't succeed due to a username/password mismatch then return to step 2
- Otherwise, dismiss the login view controller.
- If the login command was successful the resubmit the triggering commands
- If the login command failed then ask the triggering commands to send a completion notification with a failure status.
Using this pattern all of the messy exception handling logic and login presentation logic is divorced from the primary view controllers in the app. When a view controller generates a command it is blissfully ignorant of any exception handling or authentication that occurs to actually complete the request. It simply issues a request, waits for a response and processes the response. It does not care that it may have taken five retries and a user registration for the request to be completed successfully. Additionally, the service request code does not need to care about where the request originated or where the results are going, it can focus on executing the call and broadcasting the results.
The payback in implementation speed is that the developer can write the happy-path code and see demonstrable results the go back and add the exception listeners with zero impact to the happy-path code. Additionally, if designed properly, all of the network service calls can leverage the same base command class resulting in abbreviated command classes.
This pattern provides a way to rapidly show results, provide good separation of concerns between business logic and exception handling, reduces duplicate code, and provides for a better user experience than alert dialog boxes thrown from deep within network service clients.
A working example of this pattern in Objective-C can be found in a later blog article.