Part 1 of this series took an existing Ruby on Rails Web application and began the process of augmenting it to serve iPhone users. That article concentrates on the support needed on the server side to allow different content to be sent to an iPhone user, as well as allowing that user to opt out and see the full site display. Parts 2 and 3 focus on the actual content being sent to the user and how to make that content match the expectations of an iPhone or iPod touch user. Part 2 focuses on the common use of drill-down lists as a navigation method, and Part 3 focuses on forms, groupings, and other more advanced topics.
For this article, you use a Cascading Style Sheets (CSS) and JavaScript library called iUI to handle iPhone content. The iUI library has CSS classes that match Apple's human-interface guidelines for iPhone, as well as JavaScript to handle sideswipes that mimic the interface of the native iPhone OS applications. However, you aren't always going to want to use iUI in your application, so I'll also discuss some of the actual CSS and JavaScript needed to handle those common elements. In keeping with good Rails practice, I factored the HTML for common iUI patterns to Ruby helper methods. These methods are bundled in a Rails plug-in you can download and add to any Rails application.
You will continue to build on the Soups OnLine example used in Part 1. Soups OnLine was used
as an example in my book Professional Ruby On Rails and is a site
that catalogs soup recipes. In most cases, the specifics of the site are not relevant
to this article. The steps involved in putting an iPhone interface together
are not tied to the specifics of the Soups OnLine example. The only important detail is
that the application, as it exists in its pre-iPhone form, contains a controller called
RecipesController that handles listing recipes via its index method. In Part 1, a BrowsersController was added to manage opting in and out of the
Mobile Safari version of the site.
To use the examples in this article, you need a Ruby editor or IDE, such as Eclipse. A browser simulator for mimicking an iPhone display is also helpful. Options include the Aptana iPhone plug-in for Eclipse, iPhoney for the Mac, and the official iPhone software development kit (SDK) simulator. Part 1 of this series discusses the installation, usage and pros and cons of each simulator. The examples in this article use the iUI toolkit and the rails_iui plug-in.
iPhone and the user experience
On first seeing an iPhone, the casual observer notices almost immediately that it's just a tiny bit different from a traditional desktop browser. For instance, it's difficult to fit even a normal-size laptop in your front pocket. These differences have a profound effect on how you should structure your Web application for optimal user benefit on iPhone. The most important differences are:
- The screen size of the iPhone (320x480) is much smaller than even the smallest target application for a desktop Web application. The iPhone screen also has a significantly different aspect ratio from a typical desktop or laptop monitor.
- The pixel density of an iPhone is much greater than a desktop monitor, allowing small text to be read somewhat more easily and somewhat changing the relative size of images.
- Users can rotate the Mobile Safari view 90 degrees, changing the size and, more importantly, the aspect ratio of the screen.
- The touch-screen interface to Mobile Safari is less precise than a mouse interface, meaning that targets like buttons and links should be larger and farther apart than would be necessary in a desktop application.
- An iPhone is often used under slow network conditions. However, users have a strong expectation that the responses to their actions will be nearly instant.
The result of all these differences is that iPhone Web development cannot be a game of seeing how much you can cram on the screen at once. Even if you could manage to stuff all your navigation bars, logos, ad inserts, and content onto an iPhone screen, either the network slowdown would drive your mobile users crazy or they'd need to sharpen their fingers to tiny points in order to use it. Instead, the goal of iPhone Web development is a clean, simple UI that allows mobile users to get to the features that are most important. In all likelihood, some parts of your Web application will require more taps for a mobile user to access them, but the core of your application will be front and center.
By way of example, Amazon and Digg are two popular Web sites that have created iPhone-specific versions. Digg uses a variant of the iUI framework discussed in this article to mimic an iPhone look and feel, while Amazon uses a more customized look that still works extremely well in the Mobile Safari browser. A picture of Mobile Digg is shown below. (For some reason, Amazon.com doesn't simulate well.)
Figure 1. Digg for iPhone
Both Digg and Amazon have been reduced to their core functionality for mobile users — the list of top stories for Digg, search for Amazon. This focus allows the site to fit into the iPhone screen and gives the mobile user immediate access to the most important site features. Throughout the rest of this article, I show how to fit your site into an iPhone.
Adding iUI to your Rails application
There are two primary options for giving your Web application an iPhone look and feel:
- Add your own CSS and JavaScript to your site based on Apple's sample code or other good-looking sites.
- Use a pre-existing toolkit.
The most prominent of the existing toolkits is iUI. The benefit of using it is that the button images, font choices, and JavaScript effects have already been created, allowing you to focus on your site content. The downside is that it has some specific ideas about how your site should be organized:
- It expects particular Document Object Model (DOM) IDs to be used in specific places.
- Its default interaction with your server is via Asynchronous JavaScript + XML (Ajax).
I suspect that iUI is best suited for sites that can be easily conceptualized as a list. That said, Apple's human-interface guidelines for iPhone call out the list format as an "especially effective way" of organizing iPhone content, so you should probably consider list organization if you can.
iUI comes packaged as a single directory with the JavaScript file, the CSS file, and a series of images. Since Rails has specific directories where it looks for these kinds of files, you need to integrate the iUI file distribution with your Rails application by:
- Moving the iui.js JavaScript file to the public/javascripts directory in your Rails application.
- Moving the CSS file, iui.css, to public/stylesheets.
- Moving the image files (.png and .gif) go to public/images.
And since, all this movement messes up the relative URLs in the CSS file, you need
to change any reference of the form url(button.png) to
url(/images/button.png). That way, the CSS file will
correctly locate the image in the Rails distribution.
If that seems like too much trouble to do manually, the rails_iui plug-in contains a set of Rake tasks that will download and install iUI, including changing the URLs in the CSS file. The command is rake iui:install. iUI also contains compact versions of the CSS and JavaScript files, with extraneous whitespace removed for a quicker download. The file names are iuix.js and iuix.css. The automated Rake task offers the option of using the compact versions of the files.
When iUI is installed within your project, you need to add the JavaScript and CSS files to your layout. Your iPhone layout file (in this example, it's app/views/layouts/recipes.iphone.erb) should contain the following two lines in the header:
<%= stylesheet_link_tag 'iui' %>
<%= javascript_include_tag 'iui' %>
|
If you are using the rails_iui plug-in, that can be expressed merely as <%= include_iui_files %>.
At this point, you're ready to start creating iPhone content.
In the original version of the Soups OnLine application, the navigation was in a sidebar and the main content was in the center. That won't work on an iPhone, so I'm going to convert the application to a list structure. The home page of the application will include nearly the same navigation choices presented as a list, and each entry will allow a user to drill down. For example, the navigation entry for Recipes will take a user to another list that shows the most recent recipes added, with an option to show more items. Each of those entries will then link to a display page for the given recipe.
I'm going to discuss the code here on three levels:
- The Rails helper defined by the rails_iui plug-in
- The HTML generated by the plug-in using the style classes defined by iUI
- Some details about the CSS itself, for use in a non-iUI project
By default, iUI overrides the response to a normal link click. Rather than redrawing
the entire page, iUI performs an Ajax call and redraws the visible area of the page.
This allows iUI to add a sideswipe effect to each link, similar to the effect when
drilling down the artist or album list in iPhone's iPod application. You can override
this in two ways by changing the target attribute in the
anchor tag. If the link target is _self, the normal
hyperlink behavior of refreshing the whole page is used. If the link target is _replace, the anchor tag is replaced with the result of the server request.
From a Rails perspective, the iUI structure means that your main layout only gets
rendered once. After that, all calls are Ajax. Even your regular link_to calls need to be treated as Ajax calls and delivered with
:layout => false. It also means you don't need to use link_to_remote for simple Ajax activity in your iPhone Web application.
So the initial user page for this application is just the navigation. This means you
need to set up a default route for the application that renders no text of its own and
just displays the layout with the main navigation. Lacking any really obvious place to
put that route, add it to the BrowsersController created in
Part 1 by adding the following line to your config/routes.rb file: map.root :controller => "browsers".
The controller action goes in app/controllers/browsers_controller and is pretty straightforward.
Listing 1. Default layout route-controller action
layout "recipes"
def index
respond_to do |format|
format.html {redirect_to recipes_url}
format.iphone {render :text => "", :layout => true}
end
end
|
In the iPhone case, it renders just the layout with no text. If the request is for
HTML, it redirects to the RecipesController index page,
which is the main page of the desktop view of the application.
The iPhone rendering action takes place in the renderer, which now calls a couple of helper functions defined by the rails_iui plug-in to set up the page in accordance with iUI expectations, as shown in Listing 2. (The rails_iui helpers are automatically available to all views when the rails_iui plug-in is placed in the vendor/plugin/rails_iui directory of your Rails application.)
Listing 2. Layout body for iPhone main navigation
<body>
<%= iui_toolbar "Soups OnLine", new_search_url %>
<%= iui_list iphone_menu.items,
:top => content_tag(:h1, "Soups OnLine", :class => "header"),
:bottom => link_to ("Switch To Desktop View",
{:controller => "browsers", :action => :desktop},
:class => "mobile_link") %>
</body>
|
The resulting screen looks like Figure 2.
Figure 2. Main iPhone Soups OnLine navigation
There are two helper functions here. The first, iui_toolbar,
sets up the grayish-blue toolbar at the top of many iPhone applications. The Rails helper looks like Listing 3.
Listing 3. Rails helper for iUI toolbar
def button_link_to(name, options, html_options = nil)
html_options[:class] = "button"
link_to(name, options, html_options)
end
def iui_toolbar(initial_caption, search_url = nil)
back_button = button_link_to("", "#", :id => "backButton")
header = content_tag(:h1, initial_caption, :id => "header_text")
search_link = if search_url
then button_link_to("Search", search_url, :id => "searchButton")
else ""
end
content = [back_button, header, search_link].join("\n")
content_tag(:div, content, :class => "toolbar")
end
|
This code results in the HTML shown below.
Listing 4. HTML for iUI toolbar
<div class="toolbar">
<a href="#" class="button" id="backButton"></a>
<h1 dddd="header_text">Soups OnLine</h1>
<a href="http://localhost:3000/search/new" class="button"
id="searchButton">Search
</a>
</div>
|
Several items in the HTML are defined by iUI. The toolbar class defines the top toolbar color, sizing, and placement.
The h1 tag inside the toolbar is also specially defined by
iUI for the white text. The backButton DOM ID is reserved by
iUI and is created by the iUI JavaScript after a link is clicked. The header_text DOM ID is used by the next rails_iui helper. Listing 5 provides some of the relevant CSS from iUI.
Listing 5. iUI header CSS
body > .toolbar {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
border-bottom: 1px solid #2d3642;
border-top: 1px solid #6d84a2;
padding: 10px;
height: 45px;
background: url(/images/toolbar.png) #6d84a2 repeat-x;
}
.toolbar > h1 {
position: absolute;
overflow: hidden;
left: 50%;
margin: 1px 0 0 -75px;
height: 45px;
font-size: 20px;
width: 150px;
font-weight: bold;
text-shadow: rgba(0, 0, 0, 0.4) 0px -1px 0;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
color: #FFFFFF;
}
|
The second rails_iui helper, shown in Listing 6, generates the actual list from a list of menu items. The creation of the menu items objects themselves is not important in this context. (For details, check out Professional Ruby on Rails — see Resources). For the purposes of this article, a menu item is an object that has a caption and a URL attribute that describes where the menu item should go when clicked.
Listing 6. rails-iui list helper
def list_element(item)
onclick_one = "$('header_text').innerHTML='#{item.caption}'; "
onclick_two = "$('backButton').addEventListener('click',
function() {$('header_text').innerHTML='Soups OnLine'; }, false);"
link = link_to(item.caption, item.option_hash,
:onclick => "#{onclick_one} " + " #{onclick_two}")
content_tag(:li, link)
end
def append_options(list_content, options = {})
list_content = options[:top] + list_content if options[:top]
list_content += options[:bottom] if options[:bottom]
list_content
end
def iui_list(items, options = {})
list_content = items.map {|i| list_element(i)}.join("\n")
list_content = append_options(list_content, options)
content_tag(:ul, list_content, :selected => "true")
end
|
Each item in the menu list gets its own li element in the
HTML list. It contains a link to the correct URL, plus some JavaScript to manage the
toolbar caption. The JavaScript handler does two things. First, it changes the text of
the toolbar to reflect the new link. (Since the new link is just making an Ajax call to
update the body of the page, this can only be handled client-side.) Second, it changes
the handler of the Back button, so that the Back button changes the
toolbar header back to Soups OnLine. It's not a full solution to the problem of keeping
the header in synch through a deep drill-down, but as I write this, neither iUI or the rails_iui plug-in support this feature.
All the list items are put together in an HTML UL list with
the special attribute pair selected=true. iUI uses this
to determine which list to place in the body of the iPhone viewport. If there
is an HTML tag in the page where selected is set to true, the CSS assigns that to the
entire body of the viewport using the CSS declaration display:
block. In conjunction with the size definition of the body tag, this effectively gives the selected item the entire viewport. This can be helpful. In one of the iUI samples, several lists are placed on the same page to represent multiple levels of drill down. Only the one listed as selected is displayed initially, and the others are accessed via anchor and name links within the single page.
However, since the selected list is the entire viewport, the header with the Soups
OnLine logo and the footer with the Switch to Desktop View link must be placed
inside the UL tag. The helper provides options for arbitrary
HTML to be included at the top and bottom of the list — you saw them in the
earlier snippet from the layout body (Listing 2) as :top and
:bottom. The resulting HTML looks like Listing 7. I included
the entire listing for the first element in the menu, but skipped the repetitive listings for the other elements.
Listing 7. iUI list HTML
<ul selected="true">
<h1 class="header">Soups OnLine</h1>
<li>
<a href="/recipes"
onclick="$('header_text').innerHTML='Most Recent Recipes';
$('backButton').addEventListener('click',
function() {$('header_text').innerHTML='Soups OnLine'; }, false);">
Most Recent Recipes</a>
</li>
### OTHER LIST ITEMS REMOVED
<a href="/browsers/desktop"
class="mobile_link">Switch To Desktop View</a></ul>
|
Clicking on the Most Recent Recipes item gives a sideways swipe and the screen shown in Figure 3, where the header and Back button have been changed by iUI JavaScript.
Figure 3. One level of drill-down
To create this screen, the Recipe Controller's index method
needs to place format.iphone {render :layout => false} in
its respond_to block, as shown below.
Listing 8. Recipe index action
def index
@recipes = Recipe.find_for_index(params[:type])
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @recipes }
format.iphone {render :layout => false}
end
end
|
The rendered file, app/views/recipes/index.iphone.erb, uses the same rails_iui helper.
This assumes that the Recipe object can respond appropriately to the caption and option_hash methods called by
the helper: <%= iui_list @recipes %>.
Using replace to extend your list
Earlier, I mentioned that setting the target to _replace in
an iUI anchor tag causes the result of the tag call to automatically replace the
original list. This is handy to make the last element of your list display something,
such as "Next 25 items" and have the new items appear in the same list as the
originals, making it easy for the user to scroll up and down the entire list.
To get the replace feature to work using the helpers you've already built, augment the
iui_list method in two ways. The list helper
needs an option to add the More item to the list — for the moment, assume it's
an extra option at the bottom of the list. Then the response to that click needs to
return a list of items tagged li, but without the
surrounding ul tag, which already exists on the page being modified.
The first part of this implementation is some specific link_to helpers to manage the special _replace and _self behaviors of iUI.
Then I added a further method to switch between the various link types based on a target argument. Both are shown below.
Listing 9. iUI link helpers
def link_to_replace(name, options, html_options = {})
html_options[:target] = "_replace"
link_to(name, options, html_options)
end
def link_to_external(name, options, html_options = {})
html_options[:target] = "_self"
link_to(name, options, html_options)
end
def link_to_target(target, name, options, html_options = {})
if target == :replace
link_to_replace(name, options, html_options)
elsif target == :self or target == :external
link_to_external(name, options, html_options)
else
link_to(name, options, html_options)
end
end
|
With those link helpers in place, the iui_list helper and
the attendant append_options method can be augmented to allow for the new functionality.
Listing 10. iUI link helpers
def append_options(list_content, options = {})
list_content = options[:top] + list_content if options[:top]
list_content += list_element(options[:more], :replace) if options[:more]
list_content += options[:bottom] if options[:bottom]
list_content
end
def iui_list(items, options = {})
list_content = items.map {|i| list_element(i)}.join("\n")
list_content = append_options(list_content, options)
if options[:as_replace]
list_content
else
content_tag(:ul, list_content, :selected => "true")
end
end
|
The extra list element is actually added in line two of the append_options method. The element in question is expected to be passed in the :more option and, like the items list elements, is expected to have a caption and a URL. The final if statement in iui_list causes the ul list tag to be omitted if the :as_replace => true option is passed.
Calling the iui_list method with an extra final link looks
like this, where the :more option is used to provide the list element placed at the bottom of the list:
<%= iui_list @recipes,
:more => ListModel.new(nil, "Next 25 items", more_recipes_url) %>
|
The controller action that responds to more_recipes_url
— whatever that turns out to be — is expected to call iui_list with :as_replace => true.
iUI has one other trick with lists. Using the CSS group
class gives a header within the list similar to the listing used in the native iPod application for songs.
Figure 4. List with group headers
The rails_iui helper to build the group list reuses most of the code for the plain list. The method takes a block to dynamically determine the headers.
Listing 11. rails_iui helper for list with groups
def iui_grouped_list(items, options = {}, &group_by_block)
groups = items.group_by(&group_by_block).sort
group_elements = groups.map do |group, members|
group = content_tag(:li, group, :class => "group")
member_elements = [group] + members.map { |m| list_element(m) }
end
content_tag(:ul, group_elements.flatten.join("\n"),
:selected => "true")
end
|
The iui_grouped_list method uses the Rails ActiveSupport
group_by method, which converts a list into a 2-D list of
[group, [members]]. Sorting that ensures that the groups are
in alphabetical order. (You want the individual items to be placed in order before they get to this method.)
The view code for this method looks something like this (the block returns the first letter of the title of the recipe):
<%= iui_grouped_list(@recipes) {|r| r.title[0, 1]} %>
|
Where you are, and where you are going
So far, you've learned how to serve custom content for Mobile Safari users. You've also discovered how to display site navigation using a list look and feel that matches iPhone user expectations and that will load quickly even under slower Edge network conditions.
Part 3 of this series covers what to show when the user is done drilling down and actually gets to some content. This includes display of panels and dialogs and using the common iPhone rounded-rectangle style. You'll also see how to respond to the change when users rotate their devices and flip Mobile Safari sideways.
Learn
-
Start this series with "Developing
iPhone applications using Ruby on Rails and Eclipse, Part 1" and learn how to detect
Mobile Safari from a Ruby on Rails application.
-
Read
Professional Ruby
on Rails
to learn how to take a beginner Web site and make it great.
-
See Plugging
Aptana into an existing Eclipse configuration for instructions for integrating Aptana Studio into Eclipse.
-
"iPhone
on Rails -- Creating an iPhone optimised version of your Rails site using iUI and Rails
2" is a blog article about using iPhone and Rails.
-
Visit the Apple Developer Connection
for more resources for iPhone Web applications (registration required).
-
Check out the iUI toolkit.
-
Explore iPhoney.
-
Check out the "Recommended Eclipse reading list."
-
Browse all the Eclipse content on developerWorks.
-
New to Eclipse? Read the developerWorks article "Get started with Eclipse Platform" to learn its origin and architecture, and how to extend Eclipse with plug-ins.
-
Expand your Eclipse skills by checking out IBM developerWorks' Eclipse project resources.
-
To listen to interesting interviews and discussions for software developers, check out developerWorks podcasts.
-
Stay current with developerWorks' Technical events and webcasts.
-
Watch and learn about IBM and open source technologies and product functions with the no-cost developerWorks On demand demos.
-
Check out upcoming conferences, trade shows, webcasts, and other Events around the world that are of interest to IBM open source developers.
-
Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.
Get products and technologies
-
Install the rails_iui
plug-in at git://github.com/noelrappin/rails-iui.git.
-
Visit Aptana.com to download Aptana Studio.
-
Go to the Aptana Update Site to obtain the Aptana iPhone plug-in.
-
Check out the latest Eclipse technology downloads at IBM alphaWorks.
-
Download Eclipse Platform and other projects from the Eclipse Foundation.
-
Download IBM product evaluation versions, and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
-
Innovate your next open source development project with IBM trial software, available for download or on DVD.
Discuss
-
The Eclipse Platform newsgroups should be your first stop to discuss questions regarding Eclipse. (Selecting this will launch your default Usenet news reader application and open eclipse.platform.)
-
The Eclipse newsgroups has many resources for people interested in using and extending Eclipse.
-
Participate in developerWorks blogs and get involved in the developerWorks community.
Noel Rappin is the vice president of the Rails practice at Pathfinder Development (http://www.pathf.com), and has a decade of experience with web application development. He has a doctorate from the Georgia Institute of Technology, where he studied how to teach Object-Oriented design concepts. He is the author of Professional Ruby on Rails, and the co-author of wxPython in Action and Jython Essentials. Read more at http://www.pathf.com/blogs and http://10printhello.blogspot.com.



