iPhone Optimization: stylesheet_link_tag caching

Posted by Marc Love
on Friday, June 29

This change to the Rails core is a couple months old, but especially relevant when developing for the iPhone. One of the ways to optimize the performance of apps delivered to iPhones is to reduce the number of requests required for each action. If you like to split up your css files in logical order like I do, then you might find the stylesheet_link_tag caching useful.

Before:
stylesheet_link_tag :all # returns =>
  # <link href="/stylesheets/style1.css"  media="screen" rel="Stylesheet" type="text/css" />
  # <link href="/stylesheets/styleB.css"  media="screen" rel="Stylesheet" type="text/css" />
  # <link href="/stylesheets/styleX2.css" media="screen" rel="Stylesheet" type="text/css" />

stylesheet_link_tag "shop", "cart", "checkout" # returns =>
#  <link href="/stylesheets/shop.css"  media="screen" rel="Stylesheet" type="text/css" />
#  <link href="/stylesheets/cart.css"  media="screen" rel="Stylesheet" type="text/css" />
#  <link href="/stylesheets/checkout.css" media="screen" rel="Stylesheet" type="text/css" />
After:
stylesheet_link_tag :all, :cache => true # returns =>
  # <link href="/stylesheets/all.css"  media="screen" rel="Stylesheet" type="text/css" />

stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # returns =>
#  <link href="/stylesheets/payment.css"  media="screen" rel="Stylesheet" type="text/css" />

You need to have ActionController::Base.perform_caching = true for this to work.

respond_to :iPhone in Rails

Posted by Marc Love
on Friday, June 29

As promised in my prior article, here is my current solution to quickly and easily tack on an iPhone specific user interface to Rails applications. Our goal is to be able to deliver customized html, js and css for the iPhone. To do so, I’ve decided to hijack our best friend respond_to. respond_to delivers different content to browsers based on their HTTP request and even though its only designed to discriminate based on mime-type, its pretty easy to modify it to also discriminate based on user-agent. The other distinct advantage of using respond_to is that not only can we create different html, javascript and css views, but we can also pass blocks.

If you want to follow along in the core, open up actionpack/lib/action_controller/mime_responds.rb and scroll down to the Responder Class.

The initialize method checks the CGI request for the list of acceptable formats and puts them in an array in order of preference (@mime_type_priority). Later it iterates through this array and returns the first one that matches what’s listed in respond_to. Let’s modify it to automagically add iPhone specific MIME types to the front of that preference array.

alias original_initialize initialize

def initialize(controller)
  original_initialize(controller)
  # If the user agent is Safari on iPhone, duplicate the acceptable
  # formats with iphone_ prepended versions
  if @request.user_agent.include?('iPhone')
    # We only need to duplicate html, js & css mime-types
    mime_types_to_duplicate = @mime_type_priority.select do |m|
      [Mime::EXTENSION_LOOKUP['html'], Mime::EXTENSION_LOOKUP['js'], Mime::EXTENSION_LOOKUP['css']].include?(m)
    end
    # Retrieve iPhone mime-types
    mime_types_to_duplicate.map!{ |m| Mime::EXTENSION_LOOKUP[("iphone_" + m.to_sym.to_s)] }
    # and place them first in the priority list.  If they exist, they'll
    # be delivered first.  If they don't, then iPhone requests will fallback
    # on the standard html/js/css responses.
    @mime_type_priority =  mime_types_to_duplicate + @mime_type_priority
  end
end
When respond_to is called, it creates a new Responder object and calls it’s respond method. Eventually method_missing is called on the mime_type names which sends those names on to the custom method. custom generates a response for that mime-type. We have to grab these iPhone mime-types before they are processed by custom, otherwise we’ll end up sending a response to the browser with an invalid content-type header for our fake iPhone mime-types. So let’s change method_missing to this:
def method_missing(symbol, &block)
  mime_constant = symbol.to_s.upcase

  # Intercept iphone_ prepended acceptable mime-types and send them to our
  # custom_iphone response constructor
  if mime_constant.include?('IPHONE_') && Mime::SET.include?(Mime.const_get(mime_constant))
    custom_iphone(Mime.const_get(mime_constant), &block)
  elsif Mime::SET.include?(Mime.const_get(mime_constant))
    custom(Mime.const_get(mime_constant), &block)
  else
    super
  end
end

Now we need to create our custom_iphone method which creates the response just like custom except it changes the content-type header to the valid content-type:

def custom_iphone(mime_type, &block)
  mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
  # Remove the "iphone_" part of the name and retrieve the valid mime-type object
  actual_mime_type = Mime::Type.lookup_by_extension(mime_type.to_sym.to_s[7..-1])

  @order << mime_type

  if block_given?
    @responses[mime_type] = Proc.new do
      @response.template.template_format = mime_type.to_sym
      # Deliver the valid mime-type instead of our fake iphone one
      @response.content_type = actual_mime_type.to_s
      block.call
    end
  else
    @responses[mime_type] = Proc.new do
      @response.template.template_format = mime_type.to_sym
      # Deliver the valid mime-type instead of our fake iphone one
      @response.content_type = actual_mime_type.to_s
      @controller.send :render, :action => @controller.action_name
    end
  end
end
One last thing. We need to register our fake iPhone MIME types:
Mime::Type.register "iphone/html", :iphone_html
Mime::Type.register "iphone/javascript", :iphone_js
Mime::Type.register "iphone/css", :iphone_css
A little hacky? Yea. I’m not so happy with creating fake MIME types and respond_to wasn’t meant to identify user-agents, but it’ll work until I can figure out a cleaner implementation. There’s no denying the benefits of harnessing the power of respond_to. It makes developing a separate UI for iPhones super simple. Now you can do things like:
respond_to do |format|
  format.html
  format.iphone_html
  format.js
  format.iphone_js
end
Want to redirect iPhone users away from a particular method?:
respond_to do |format|
  format.html
  format.iphone_html{ redirect_to( :action => "index") }
end

No need to modify any of your other application code or existing views. Just add the .iphone_* templates (i.e. index.iphone_html.erb, index.iphone_js.erb) and modify your respond_to blocks. GOTCHA ALERT: regular and iPhone-specific html templates will share the same layout if one is designated at the controller level. To hide parts of your layout files from iPhone requests, just do:

primary_layout.html.erb:
<% unless request.user_agent.include('iPhone') %>
Hidden from iPhones
<% end %>

Development for iPhone

Posted by Marc Love
on Wednesday, June 27

Normally I wouldn’t advocate developing completely different views for one particular user-agent. It violates the web standards mindset that has been burned into my brain over many years. However, I can’t ignore the fact that the iPhone throws a wrench into some of that mindset. The iPhone’s full web browser will be great for reading news sites & blogs, but for web apps, we should take a couple things into consideration:

  • Every kilobyte matters! The EDGE network will have users crawling the web, quite literally. Download speeds will float somewhere between that of a 56kbps modem and an ISDN line when users are away from WiFi. Hiding elements with media-specific stylesheets does us no good because the data is still downloaded (including all those hefty javascript libraries). Which brings me to my next point…
  • iPhone will probably use “screen” css media, not “handheld” (pretty clear from the public demonstrations and Apple’s huge emphasis on it having a “FULL web browser” and browsing the web the “way it was meant” to be browsed). After all, what’s the point of having a phone with a full web browser complete with zoom-in, zoom-out capability if the browser loads stripped down versions of web pages by default? Again, great for browsing, not for monitor-sized web apps.
  • Using web apps on the iPhone will be cumbersome enough as it is. There’s no way to directly access them from the home menu. Your customers will have to open Safari, find their bookmark or manually type in the address, and login … username (or worse email) and password. That’s a lot of navigating & typing just to get into the application. Hopefully Apple will develop quicker access methods, but until then the user experience has to be excellent to make it worthwhile to the customer.
  • User interaction with an application on the iPhone is fundamentally different than other “screen” media types. We’re using our fingers, not a mouse. Controls need to be big, finger-friendly and should take advantage of the tactile interactions that are by default a part of the user’s experience. And don’t forget that all your hover and mouse-triggered javascript events will never be triggered.

So assuming you’re building an application or enhancing an existing application to be competitive in the iPhone market, it makes sense to create an iPhone-specific UI. I’m working on a solution for Rails which will make tacking one on super simple which I’ll post shortly.

Life on Edge Rails

Posted by Marc Love
on Friday, June 01

assert_difference & assert_no_difference now accept an array of expressions

The assert_difference and assert_no_difference testing methods now can accept an array of expressions to evaluate. So now instead of doing:


assert_difference 'User.count', +1 do
  post :create, :article => { :profile => {}, ... }
end
assert_difference 'Profile.count', +1 do
  post :create, :article => { :profile => {}, ... }
end

we can do:


assert_difference ['User.count', 'Profile.count'], +1 do
  post :create, :article => { :profile => {}, ... }
end

That’s some nice DRY action!