The iPhone supports several forms of networking: Wi-Fi, 3G, and 2G
EDGE. The iPhone will detect and use the fastest connection
automatically. That is, if Wi-Fi is available, the iPhone will use it. If
not, it will attempt to use a 3G connection. Failing that, it will use
2G.In many circumstances, there will be no connectivity. For example,
there is rarely a connection in the New York City subway system.
Applications that rely on networking can often take steps to provide
utility even in the absence of network connections. A great first step is
saving any data currently in use when an application terminates. This
might be web-based data like HTML pages, RSS feeds, or cached image files.
Another good idea is handling any pending transactions between the device
and remote servers. Network programming deals with data transfer and
communication between machines. If a user loses network access while using
an application, the application should act appropriately. In some cases,
that will mean showing an alert that notifies the user of catastrophic
failure. In many cases, though, a better option is simply to save any
uncommitted transactions and present some sort of peripheral indication
like an icon.
Figure 1 shows Shovel, an iPhone app for reading content from Digg. The screen
displays the application as users see it when they have no network
connection. A great enhancement to applications that deal with massive
datasets might be to cache any data that loads, so users can revisit content in
the absence of connectivity.
1. Maintain State and Persist Data
Applications often use network connectivity to transfer data from
remote servers or systems. A good example of a networked application is an email
program such as the Mail application. Email messages are transferred
across the network to the iPhone and stored locally. Maintaining state
lets users read their email even in the absence of a stable network
connection that would allow the delivery of new messages. Many networked
applications can remain useful and relevant to users in the absence of
connectivity by displaying stored versions of transferred data.
There are many ways to save application data on the iPhone. One
option is to structure your application data using a standard Cocoa data
structure like an NSArray or NSDictionary, archiving the entire
structure to a binary file. A second option is to use property
list serialization, saving your data in a structured XML
document. Finally, the most robust method of data storage on the iPhone
is to use SQLite—a database that is included with the iPhone OS—or
Core Data, a powerful framework for data management.
2. Cache User Input
Reading data from the Internet is a very common requirement for
mobile applications. A related requirement is to write data to network
services. The presence and reliability of network services adds an
element of risk for users: if connectivity drops or is absent, the state
of both remote and local data becomes ambiguous. A standard pattern for
ensuring state across network connections is to rely on return codes
from the remote service. Applications can use representational state
transfer (REST) over HTTP to simplify their
communication and leverage return codes to ensure that local state and
remote state match up where
appropriate.
The communication protocols are less important than the use of
networking. You should treat network connectivity as an enhancement
rather than a requirement if possible.
What does this mean for an application focused on networking?
Consider two examples: the built-in Mail application and any of the
excellent Twitter clients for Cocoa Touch. In both cases, the ability to
communicate with remote services is integral to the application. An
email client must at some point—and preferably all the time—connect to
the Internet. The same is true of a Twitter client. In such cases,
networking is more than a friendly enhancement.
The requirement for Internet connectivity might be described in
one of two ways:
The second description lets us create a friendlier user
experience. The ideal networked iPhone application is a client of
network services, but networking is only one facet.
The Mail application solves the issue of network requirements.
Email messages are synchronized to the device and remain readable in the
absence of an Internet connection. Messages or replies are composed on
the device and automatically saved locally in the Outbox (ready for
delivery) or in the Drafts folder (marked as incomplete, but still
saved). When a network connection is available, messages in the Outbox
are sent to the appropriate mail server. If no connection is available,
Outbox messages remain on the device, ready to go.
The ubiquity of this pattern in desktop email clients has set user
expectations, and Apple delivered (no pun intended) by sticking to
standard behavior. The same pattern is useful with any networked
application. Any application can use the Outbox metaphor to address the
natural separation of functionality: the user composes content, and the
user transfers content. The two functions aren’t necessarily linked
together, though the latter relies on the former.
If you choose to address network writes this way, you should pay
special attention to the mechanisms that notify users when messages are
transmitted—and when transmission fails. One area in which Apple fails
to deliver a smooth user experience is in the notification that mail
cannot be sent. Mail treats a lack of connectivity as a failure state,
and a full-screen modal alert is used to notify users. This alert
abruptly interrupts the task at hand, such as composing a second
message. Further, the user is required to click a button to dismiss the
alert. A navigation prompt would be a more subtle mechanism for showing
low-priority alerts. This example shows a prompt that displays the
message “Offline” to a user:
- (void)showOfflinePrompt
{
self.navigationItem.prompt = @"Offline";
[self performSelector:@selector(hidePrompt) withObject:self afterDelay:2.0];
}
- (void)hidePrompt
{
self.navigationItem.prompt = nil;
}
3. Reflect Connectivity Appropriately
Progressive enhancement is the art of using available resources to
provide the best possible experience for users, and elegantly adapting
to changes in resource availability. The way applications communicate available functionality
and the state of data is a topic worthy of attention. An application can
use several mechanisms to alert users to events or changes in state.
Developers should carefully choose alerts that reflect the importance of
an event. Most changes probably don’t require abrupt interruption and
the full attention of a user.
You can show simple status updates through iconography if the
icons are universally familiar. A good example is the network status
display on the iPhone. The display alternates between several
states:
If you create a full-screen application that uses network
connections in a significant way, the native iPhone iconography may be a
good foundation for your indicators and visibility.
4. Load Data Lazily
Lazy loading is a great data management pattern for progressive
enhancement. It refers to the art of retrieving only the data you need
to display or search at a given moment, which helps reduce latency and blocking of the main thread. The
use of lazy loading is particularly helpful when working with large
datasets or when traversing complex collections of objects. Working with
UINavigationController instances to drill through
hierarchical data provides a good opportunity for lazy loading,
especially when reading data from an SQLite database.
To better understand lazy loading as it relates to
navigation-based applications, consider a dataset that acts as a
directory of companies and their employees. Figure 2 shows a simple database schema with
example rows. Imagine hundreds of companies listed alphabetically in a
UITableView.
The normal pattern for displaying this information would be to
provide users with three levels of views: all companies, all employees
for a selected company, and all information for a selected
employee.
Implementing lazy loading for the screen that displays all
companies would include selecting the name property
of each Company record from the database with a query
such as SELECT name FROM companies; and storing the
results in an NSArray that can be used to populate
table cells. Selecting a table cell representing a
Company will push a new
UIViewController onto the navigation controller
stack, and a query will retrieve information for that controller. In
this example, a developer would load all employee names for the company.
A query such as SELECT name FROM employees WHERE company_id =
1001; would retrieve only the information needed for display
and no more.
Developers accustomed to working on high-traffic database systems
often have to balance the number of database calls with the
weight of each call—that is, the amount of data
being read and returned for a given query. Using SQLite on the iPhone is
essentially immune to those concerns, and developers can safely retrieve
additional data.
You can go further when loading data lazily by restricting not
only the fields or associations loaded for an object, but also by
loading only a subset of objects. The App Store loads search results in sets of 25 and displays
a button at the bottom of the list that allows users to load an
additional 25 records, adding the new objects to objects already in
memory. This is different from pagination, which is a pattern for
replacing subsets with new subsets. Modifications to the queries to
leverage subset loading is simple:
SELECT name FROM companies ORDER BY abbreviation, id ASC LIMIT 25;
SELECT name FROM employees WHERE company_id = 1001 ORDER BY name ASC LIMIT 25;
Figure 3 shows the App Store offering
to load yet another 25 tip calculators.
It’s possible to use lazy loading when working with file-based
storage such as serialized objects or property lists, but doing so
requires developers to manage multiple disparate files. For example, you
might have all Company objects serialized in an
NSArray object and saved to one binary file, and all
Employee objects serialized in an
NSMutableArray and stored in a separate file. This
approach will work, but it’s typically riskier than using SQLite because
of the risk of file corruption, the inability to query for partial sets
of records or partial attribute sets, and the lack of relational
integrity that SQLite provides.
If your dataset is small—a few megabytes, for example—serializing
a single collection is fairly safe. You won’t have support for fast
queries as with SQLite, but the overall management of your objects will
be much simpler. Starting with the simplest option is a very smart move,
but you should always keep an eye on the limits of the persistence
mechanism you choose. If you find yourself splitting your object graph
into multiple graphs and serializing them into separate files, it may be
time to explore SQLite.
4.1. Lazy loading with Core Data
Core Data is a new feature for the iPhone with iPhone OS 3.0.
With Core Data, developers access data using
managed objects. A managed object is a special object in Cocoa that represents an entity
in your application. When you work with Core Data, you can access
objects directly. This approach differs from that of SQLite. With
SQLite, you must query the database for data that is stored in rows of
columns, like a spreadsheet. Once retrieved, the rows must be
converted to objects using tedious conversion processes, like assigning properties
using the values in rows. With Core Data, you don’t store objects
directly as decomposed rows of data in a database. Instead, you insert
an object or set of objects into a managed object
context, which serves a similar purpose to a database. In
fact, on iPhone OS, the underlying storage mechanism for managed
objects is SQLite.
You access managed objects by asking the managed object context
for a set of objects that fit certain criteria. Once fetched, objects
can manage their own lazy loading using a mechanism known as
faulting. For example, if you have a managed
object representing a Company, and that
Company has relationships representing a set of
Employee-managed objects, you
can fetch the Company from your managed object
context first, then simply access the related
Employee instances as properties of the
Company. If the Employee objects
aren’t fully loaded into memory, Core Data will recognize the problem
and fetch the missing data. This is a form of lazy loading that is
built into Core Data.
The problem with lazy loading using Core Data is twofold. First,
it’s expensive. Your managed object context has to make a round trip
to its underlying storage mechanism to read the data and assign it to
managed objects. The process is similar to manual lazy loading using
SQLite, though less work is required for application developers
because Core Data supplies objects rather than raw SQLite
records.
The second problem is that Core Data manages data locally and is
highly optimized for the task, making the type of lazy loading
required for SQLite unnecessary. Typical applications will have no
need for lazy loading using Core Data, and if they do, the built-in
faulting mechanism will load the required data as needed.
Core Data provides methods for fetching not only objects that
meet certain criteria, but also related objects that are likely to be
accessed and trigger lazy loading. Fetching sets of related objects,
including all of their data, can consume available memory very
quickly.
The power of Core Data comes at a cost when compared to a
database or file-based serialization. Core Data is a robust framework.
The balance between memory and processing time is a classic problem
for UX programmers, but the flexibility of Core Data allows developers
to design relatively efficient systems.
In short, Core Data deserves diligent study by anyone building
Cocoa or Cocoa Touch applications.
5. Peer Connectivity with GameKit
The iPhone OS version 3.0 includes a framework called
GameKit. Applications use the GameKit framework
to enable peer-to-peer connectivity across Bluetooth. As the name
implies, this feature is most useful when developing games, but non-game
applications can also take advantage of GameKit to
add fun features. For example, the App Store will soon include dozens of
applications that let users exchange virtual business cards.
GameKit is an intriguing addition to the
iPhone, and it will allow developers to create all kinds of interesting
applications. Like all networking services, GameKit
should be treated as an enhancement if possible. An application for
exchanging business cards should provide functionality for users who are
not in a peer-to-peer environment. A multiplayer game that uses
GameKit sessions to foster group competition should
provide a single-player mode for users unable to play against
others.
Unlike simple Internet connectivity, peer-to-peer connectivity
requires at least two users in close proximity with the same application
installed and running. Additionally, those two users must agree to
connect their devices via Bluetooth. Applications that treat
peer-to-peer connectivity as an exciting option rather than a
requirement will seem more valuable to users, leading to greater
adoption.