Abstract

Force.com Mobile

This article looks at the development of a custom user interface for a Force.com Mobile iPhone application. For an introduction to mobile Force.com application development, see An Introduction to Force.com Mobile Application Development.

Setting the Scene

As a developer, you can use Visualforce and Apex Code to write sophisticated and powerful applications that take advantage of Force.com - but what about for mobile applications? An interesting feature of the Force.com Mobile client is the ability to use Visualforce for application development when you want to go beyond the point-and-click configuration and setup of your mobile app.

Here are some tips to consider before starting to develop mobile apps for the iPhone:

  • If you are developing on OS X, you should download and install the Force.com iPhone Simulator for Mac OS. The simulator is the best way to quickly test your work, and the behavior is identical to an actual iPhone.
Safari Develop Menu
  • For all developers (OS X & Windows), you will want to leverage the developer tools found in the Safari web browser in order to do rapid development and testing. To use Safari for testing, go to Preferences...|Advanced menu option and select the Develop checkbox. This will enable the Develop menu in your browser.
Using the Develop menu, you can select to emulate the embedded browser on the iPhone by choosing the appropriate User-Agent. This will allow you to test your pages, but you will still need to test on the iPhone. The Force.com Mobile client uses the appropriate mobile configuration to fetch and maintain data on the device, so you will have to test using the iPhone or the simulator in order to see how your application will behave in the real world.

Using Safari for development and testing works for both Windows and OS X. With the Developer setting enabled, you will also have access to a Firebug-like set of debugging tools to inspect CSS elements and other constructs on your page.

Extending our use case

In An Introduction to Force.com Mobile Application Development we mobile-enabled the Accounts object from our Developer Edition environment. While this was fine to demonstrate how easy it is to expose Force.com objects on the mobile device, it falls short of showing off what the potential of the mobile platform really is.

In this article we will extend the application to introduce custom rendering of data. In particular, we will map our accounts by customer rating using the Google Maps API. This example uses the default data contained in your Developer Edition environment (some data setup for this example is required - see next section).

Setting up your environment

The following example uses address data from Accounts to geocode positions in Google Maps. You will have to update the sample data that comes with your developer edition, in order to ensure that all address fields contain valid data. The default data set in Developer Edition has the Account address data concatenated and stored in the Street field. We will need to ensure that the Accounts we use have complete data in all address fields (street, city, state, zip). For this example, you should modify your data as follows:

1. Accounts - Update and complete the address data contained in the following accounts:
  • Edge Communications
  • GenePoint
  • United Oil & Gas Corp
2. Modify your User info and set your address to be in proximity of the above company addresses - this will make it easier to see all of the accounts on the small map display on the iPhone. In my example, I have set up all of the above accounts to be located in the San Francisco Bay area.

Embrace the cloud - leveraging Google Maps

Google Business Search

I thought it would be an interesting exercise to mimic the way that Google Maps displays search results for businesses (see image to the right). For our exercise, we will map our accounts by Rating and display the results on a map with lettered markers.

Furthermore, since we are working on an iPhone with limited screen real estate (320x480px), we will place the map above the list of business names (accounts in our example). To make this more interesting, let's use different colored markers depending upon the account Rating:

  • Hot = Red marker
  • Warm = Yellow marker
  • Cold = Green marker

Now that we have our set up considerations out of the way, let's begin coding!

Mobile Visualforce

If you are familiar with development techniques for Visualforce, there are just a few additional points to note before we dive into our code. If you are new to Visualforce, you can review the excellent primer An Introduction to Visualforce.

  • Keep it light - Custom controllers are a best practice for mobile development, as you want to ensure that only the requisite data is fetched and processed into your resulting HTML that is sent to the device over the air. Keep in mind that your Visualforce page is sent to the device as HTML - minimizing this payload will make for a more responsive page (initial load time).
  • Custom stylesheets - Each device has a particular look and feel that is respected by the Force.com Mobile client. Your Visualforce pages should conform to the particular device look and feel as well. For our iPhone exercise, we will we explore a way of accomplishing this design task.

Delivering our custom app to the iPhone is the goal, so adopting the iPhone look and feel is a key requirement.

I'd Like to Buy a Vowel, Please

As you would expect, there are many code projects and iPhone CSS frameworks in the public domain. Two such open source projects that caught my attention were:

Reviewing these projects, I was drawn towards Joe Hewitt's iUI because of its simplicity and small footprint (47Kb). Fellow evangelist Dave Carroll also had some ready experience with iUI, so this made my decision an easy one.

Let's take a look at a simple Visualforce page using iUI and adhering to best practices discussed in the external article Apple iPhone design guidelines:

<apex:page controller="My_Custom_Controller" showHeader="false">
    <meta name="viewport" content="width=320; initial-scale=1.0; maximum-scale=1.0; 
           user-scalable=0;"/>

    <apex:styleSheet value="{!URLFOR($Resource.IUI, 'iui-0.13/iui/iui.css')}" />
    <apex:includeScript value="{!URLFOR($Resource.IUI, 'iui-0.13/iui/iui.js')}" /> 
    
    <style>
    	#home { position: relative; top: 0px; }
    </style>
  
    <ul title="Accounts" selected="true" id="home" >
       <!-- Draw user name in panel banner--> 
       <li class="group">User: {!$User.FirstName} {!$User.LastName}</li>
        
       <!-- Create panel, insert text -->
       <div class="panel" style="padding: 10px;"  >
          Hello iPhone!
       </div>
        
       <!-- Draw title for accounts in panel banner-->
       <li class="group">Account(s)</li>
        
       <!-- Draw accounts, one per row -->
       <apex:repeat value="{!MyAccts}" var="p" >
           <li>
              {!p.Name}
           </li>  
       </apex:repeat>
    </ul>

</apex:page>


This code outputs the following page (viewed in Safari):

iUI Visualforce Page


This sample shows how easy it is to leverage the iUI framework to produce an iPhone UI with Visualforce. You should note that I overrode the #home style from the iUI library. I did this ensure that our app will render correctly within the Force.com Mobile client for iPhone - that is, without any noticeable gap at the top of the page on our device. I uploaded the entire iUI library as a static resource (see Delivering Static Resources with Visualforce).

Another design consideration is avoiding the use of the class="Toolbar" element at the top of your detail pages. While it may be tempting to provide for a iPhone-style navigation buttons in the toolbar at the top of your page, this will result in a confusing user experience due to multiple navigation controls at the top of the page. If you wish to use these button styles (provided in the iUI framework), you should not use the Toolbar class to render your buttons.


Custom Controller

This example uses a custom controller to fetch and prepare our records for display. Let's take a look at the controller code:

public class My_Custom_Controller {
	
   public Account[] getMyAccts() {
      String usrId = UserInfo.getUserId();
      Account[] accts = [Select Id, Name, Rating From Account 
                                where Rating != ''
                                And BillingPostalCode != ''
                                And OwnerId =: usrId ];		
       
       return accts;     
   }
	
}


The above controller implements my business logic to get my accounts (getMyAccts()) that have Rating data and a Postal Code.

Use of Templates

In the above page, you can see how we set up the use of the iUI framework by leveraging the include and script tags. This pattern will repeat itself on all iPhone list and details pages you build with Visualforce.

Luckily, Visuaforce provides a way of creating a template that will allow you to include page elements as well as variables to be replaced at run time. More information on Visualforce Templates can be found in the Visualforce documentation.


In our example, replace this chunk of common code:

<apex:page controller="My_Custom_Controller" showHeader="false">
    <meta name="viewport" content="width=320; initial-scale=1.0; maximum-scale=1.0; 
           user-scalable=0;"/>

    <apex:styleSheet value="{!URLFOR($Resource.IUI, 'iui-0.13/iui/iui.css')}" />
    <apex:includeScript value="{!URLFOR($Resource.IUI, 'iui-0.13/iui/iui.js')}" /> 
    
    <style>
    	#home { position: relative; top: 0px; }
    </style>
    .
    .
</page>


with the following instantiation of the template "iuivf":

<apex:page controller="My_Custom_Controller" showHeader="false">
   <apex:composition template="iuivf" >
      <apex:define name="body-content" />
   </apex:composition>
    .
    .
</page>


The <apex:composition> tag references the page template "iuivf", which is defined as follows:

<!--
*   Page definition: iuivf
*   Visualforce template for iUI includes needed for
*   using the iui framework <http://code.google.com/p/iui/>
*   in any Visualforce page.
-->

<apex:page >
   <meta name="viewport" content="width=320; initial-scale=1.0; 
      maximum-scale=1.0; user scalable=0;"/>
   <apex:includeScript value="{!URLFOR($Resource.IUI, 'iui-0.13/iui/iui.js')}" /> 
   <apex:styleSheet value="{!URLFOR($Resource.IUI, 'iui-0.13/iui/iui.css')}" />
    
   <style> 
      <apex:insert name="body-content"> 
         #home { position: relative; top: 0px; } 
      </apex:insert>
   </style>
    
</apex:page>


Deploying to the iPhone

While you can conduct preliminary testing using Safari, you ultimately need to test your development work on the iPhone. With your Visualforce page complete and ready for testing, follow these guidelines for testing with the iPhone Simulator or on the iPhone itself:

1. Log in to your development org
2. Create a Visualforce Tab for each of your pages
3. Modify your mobile configuration
4. Add your Visualforce Tabs to the Mobile Tabs section of your mobile configuration
5. On your iPhone (or simulator), log in to your org using the Salesforce Mobile client

At this point, you will be able to test your Visualforce pages within the constraints of the iPhone and Salesforce Mobile platform.


Visualforce Design Patterns with iUI

Referencing the above Visualforce and Apex controller code, we can see the following pattern emerge:

1. Define the page using a Custom Controller
2. Use the viewport meta tag as prescribed
3. Override #home for correct placement in Force.com Mobile client UI
4. Include the iUI framework using the <apex:styleSheet> and <apex:includeScript> tags.
5. Use a Visualforce Template for your boilerplate iUI calls and overrides. Use the <apex:composition> tag to reference your template in each of your iPhone Visualforce pages.
6. Use the embedded browser navigation, instead of trying to use the Toolbar class with iPhone-style buttons. The Force.com Mobile client provides for standard browser controls (back/forward/refresh) for all Visualforce pages by default.


It's All In the Details

Let's take our example to the next level by providing a means for us to view a detail page for each of the Account records referenced in the list view. A minor edit will allow us to take advantage of iUI's style for linking rows to other pages.

<apex:repeat value="{!MyAccts}" var="p" >
   <li>
      <apex:outputLink value="accountDetail?id={!p.Id}">
         <apex:image id="{!p.Id}" value="http://maps.google.com/mapfiles/marker.png" />
         {!p.Name} 
      </apex:outputLink>
   </li>  
</apex:repeat>

This code modification will now output the following page (viewed in Safari):

Accounts with Links

A few things to note in our above code:

  • Wrapping our row in a href tag automatically provides for the iPhone link look and feel
  • Reference the outputLink target using standard Visualforce syntax, using the page name for the link target
  • I am using Google Maps markers (image next to the account name) to provide a teaser for our next section, where we build out our example to include a Google Maps element.

The detail page will look as follows:

Detail Page

Here's the Visualforce markup to generate the above page. We've put this in the accountDetail Visualforce page.

<apex:page showHeader="false" controller="customAccountController" title="My Account" >
    <apex:composition template="iuivf" />
    
    <div class="panel" id="acctDetail" selected="true" style="padding: 10px; 
      margin-top:-44px" title="Account Info" >

       <h2>{!Account.Name}</h2>

       <fieldset style="margin: 0 0 20px 0;">

          <div class="row">
             <label>Id:</label>
             <input type="text" value="{!Account.Id}" />
          </div>
            
          <div class="row">
             <label>Rating:</label>
             <input type="text" value="{!Account.Rating}" />
          </div>
            
       </fieldset>
        
    </div>
</apex:page>

The above detail page markup contains some key iUI classes that we will use liberally in our iPhone Visualforce projects:

  • panel: Similar to our <apex:OutputPanel>, used to control placement of other elements
  • fieldset: A collection rows rendered on a roundrect background
  • row: iPhone styled row - can contain any other HTML element


A Picture is Worth a Thousand Words...

At this point, we have all of the ingredients needed to build out our mapping application outlined earlier in the article.

While Google Maps and Visualforce is a proven commodity by now, we can still seek to discover new ways to provide tight binding of data between our Force.com app and Google Maps. Much of the code below uses familiar Google Maps constructs, so I will take the liberty of only discussing those bits that are unique to our running as Visualforce page on the iPhone.

Unique to our requirements are:

  • Coordinating the markers on the map and the list in our custom UI
  • Constrained screen real estate
  • Performance optimization for mobile devices

Let's take a look at our finished app, before we do a final code review.

Finished App


Maps Visualforce Page

The page above builds upon our earlier example, and includes the map definition - referenced by id="map" in the code below:

<apex:page controller="My_Custom_Controller" showHeader="false">
    <apex:composition template="iuivf" />
	
    <script src="{!myKey}" type="text/javascript"> </script>
	
    <script type="text/javascript">
    
    function addLoadEvent(func) { 
       var oldonload = window.onload;
       if (typeof window.onload != 'function') {
          window.onload = func;
       } else {
          window.onload = function() {
             oldonload();
             func();
            }
        }
     }
 
     addLoadEvent(
     function() {
        if (GBrowserIsCompatible()) {
           var my_geocoder = new GClientGeocoder();
           var map = new GMap2(document.getElementById("map"));
           var TC = new GMapTypeControl();
           var bottomRight = new GControlPosition(G_ANCHOR_BOTTOM_RIGHT, new GSize(10,10));
           var mCount =0;
		        
           map.addControl(new GSmallMapControl()); // small arrows
           map.addControl(TC, bottomRight);  // map type buttons
		                           
           function LTrim( value ) {
              var re = /\s*((\S+\s*)*)/;
              return value.replace(re, "$1");
           }
		
           function RTrim( value ) {
              var re = /((\s*\S+)*)\s*/;
              return value.replace(re, "$1");
           }
		
           // Remove leading and ending whitespaces
           function trim( value ) {
              return LTrim(RTrim(value));
           }
		                                    
           function doAddLocationToMap(SiteName, Street, City, State, Zip, typ) {
              var addr = Street + ", " + City + ", " + State + " " + Zip;
              my_geocoder.getLatLng (addr, 
              function(point) {
                 if (point) {
                    var mTag = '';
                    var myIcon = new GIcon(G_DEFAULT_ICON);
			                    
                    if(typ == 'self') {
                       mTag = "<b>" + SiteName + "</b>" + "<br>" + City ; 
                       myIcon.image = "http://maps.google.com/mapfiles/arrow.png";
                       myIcon.iconSize=new GSize(32,32);
                    } else { 
                       if(typ == 'acct') {
                          mCount ++;
                          var priAr = SiteName.split(":"); 
                          var compName = priAr[0];  // company name
                          var pri = trim(priAr[1]); // rating
                          var acctId = priAr[2]; //account id
                          var index = "";
                          var imgName = "marker"; // default marker image
                          var color = ""; 
				                        
                          mTag = "<b>" + compName + "</b>" + "<br>" 
                                 + "Rating: " 
                                 +  pri  + "<br>" + City ; 
										
                          // set up marker colors based on priority
                          if (pri == 'Warm') color="Yellow"; 
                          else if (pri == 'Hot') color="Red"; 
                          else if (pri == 'Cold') color="Green";
										
                          if(mCount>10){ // use default marker
                             myIcon.image = 
                                "http://maps.google.com/mapfiles/marker.png";
                          } else { // use custom marker 1-10
                             index = String(mCount);
                             imgName = imgName + color + index + ".png";
                             myIcon.image = "{!URLFOR($Resource.markers, 
                                            'markers/" + imgName + "')}";  
                          }
										
                          document.getElementById(acctId).src = myIcon.image;
                          myIcon.iconSize=new GSize(20,34);
                       }
                    }
                    myIcon.shadowSize=new GSize(56,32);
                    myIcon.iconAnchor=new GPoint(16,32);
                    myIcon.infoWindowAnchor=new GPoint(16,0);
                    markerOptions2 = { icon:myIcon };
                    var marker = new GMarker(point, markerOptions2);
                    map.setCenter(point, 8);
                    map.addOverlay(marker);
			                                
                    // setup listener action to show info on click event                       
                    GEvent.addListener(marker, "click", 
                       function() { 
                          marker.openInfoWindowHtml(mTag); 
                       }) ;		                                           
                 }
              }
              );
           }                           
		                                                               
           //get accts and draw address
           var arAllStr = '';
           arAllStr = '{!AddrArStr}'; // Get all address recs 
           var arLi = arAllStr.split("~::~"); // Split on line break delim
           for (var i = 0; i < arLi.length-1; i++) {  
              var arLiStr =arLi[i];
              var arCols =arLiStr.split("~:~"); //Split  to get columns
		              
              if(arCols[1].length >0)
                 doAddLocationToMap(arCols[0],arCols[1],arCols[2],
                                    arCols[3],arCols[4],'acct');     
           }
		                                    
           //get user address and draw
           doAddLocationToMap('{!$User.FirstName} {!$User.LastName}'
                 +' (Me)','{!$User.Street}','{!$User.City}','
                 {!$User.State}','{!$User.PostalCode}','self');                                           
        } 
    }
    );
    </script> 

    <ul title="Accounts" selected="true" id="home" >
    	<!-- Draw user name at top of panel --> 
        <li class="group">
            User: {!$User.FirstName} {!$User.LastName}
        </li>
        
        <!-- Create panel for Google Maps object -->
        <div class="panel" style="padding: 10px;"  >            
        	<div id="map" style="width: 300px; height: 300px;"> 
            </div>
        </div>
        
        <!-- Create group sub-panel to display list -->
        <li class="group">Account(s)</li>
        
        <!-- Draw accounts, one per row -->
        <apex:repeat value="{!MyAccts}" var="p" >
           <li>
              <a href="accountDetail?id={!p.Id}" >
                 <img id="{!p.Id}" 
                      src="http://maps.google.com/mapfiles/marker.png">
                 {!p.Name} 
              </a>
           </li>  
         </apex:repeat>
    </ul>
</apex:page>


Visualforce Code Highlights

  • iUI framework is included from the static resource $Resource.IUI
  • The code gets the Google Maps API key using the {!myKey} reference
  • Binding the markers in the list to the markers on the map was accomplished by referencing the img id of the map marker. Don't be confused by the use of src="http://maps.google.com/mapfiles/marker.png" - this is done to provide for a placeholder for the image referenced by the img id, which will be updated to the referenced image id after initially painting the map.
  • The accounts to map are processed by the controller and referenced in the page via the String array {!AddrArStr}.
  • The marker image names on the map are indexed from 1-10, and refer to images contained in the static resource $Resource.markers. For background on using static resources, refer to Delivering Static Resources with Visualforce.

The bulk of the javascript sets up the Map object by performing the following logic:

  • Using Simon Willison's addLoadEvent() technique, call our function to prepare and paint the map on page load
  • Get the addresses to map from the string array {!AddrArStr}
  • Unpack the array of addresses by keying off of the delimiters set up in the controller code
  • Call doAddLocationToMap for all account addresses and for current user
  • Use Account.Rating as key to determine what color marker to use (green, yellow, red)
  • The custom image markers are stored in the static resource $Resource.markers


My Custom Controller

Following best practices, we have created a custom controller for our page referenced above. Our custom controller will allow us to only reference and access those objects needed for our app, thereby providing for a level of processing optimization that will help our app perform well.

/*
*   Controller definition: My_Custom_Controller
*   This controller is used with the page MapAcctd to map
*   locations (Accounts) that the user owns and have rating = "hot"
*
*   Assumptions:
*       - Account data Exists (no null set on query)
*
*   To Do:
*       - Handle exceptions
*		- Provide test data
*
*   Written by: Mike Kreaden 03/15/09
*/

public class My_Custom_Controller {

   public String addrStr;
   public User usr;
   public String myKey;
	
   public Account[] getMyAccts() {
      String usrId = UserInfo.getUserId();
      Account[] accts = [Select Id, Name, Rating, OwnerId,  
                         BillingStreet, BillingCity, BillingState, 
                         BillingPostalCode
                         From Account 
                         where Rating != ''
                         And BillingPostalCode != ''
                         And OwnerId =: usrId ];
                            
      for(Account acct : accts) {            
         addrStr = addrStr + acct.Name + ' : ' 
                   + acct.Rating  + ':' 
                   + acct.Id + '~:~'+ acct.BillingStreet + '~:~' 
                   + acct.BillingCity + '~:~' +  acct.BillingState + '~:~' 
                   + acct.BillingPostalCode + '~::~';		
      }

      return accts;     
   }
    
   public String getmyKey() {  // set up google maps api key
      myKey = 'http://maps.google.com/maps?file=api&v=2&';

      // in the following line, enter your google maps key
      // to get an api key, visit the Google Maps API site
      // http://code.google.com/apis/maps/signup.html
      myKey = myKey + 'key=<insert_google_maps_api_key_here>';

      return myKey;
   }
    
   public String getAddrArStr(){
      addrStr = '';
      Account[] theRecs = getMyAccts();

      return addrStr;	
   }
	
}


The above controller code fetches and processes Account data in the following manner:

  1. Retrieve my accounts with Rating data and BillingPostalCode data
  2. Build string array of delimited accounts for use in the mapping javascript routine on the Visualforce page
  • Field delimiters = "~:~"
  • Record delimiters = "~::~"

Additionally, I have set up a getter for my Google Maps API key. To use this code, you will have to sign up for your own key on the Google Code site.


Summary

This article introduces a way to create mobile applications for the iPhone on the Force.com platform. It shows how to use the open presentation framework, iUI, together with Visualforce and the Force.com Mobile client to create pixel-perfect iPhone layouts, and how to set up pages and controllers to conform to Mobile Visualforce best practices.

If you are interested in the source code and resources for the project outlined in this article, check out the link in the References section below to an unmanaged package containing the Visualforce pages, Apex Code and Static Resources referenced in this article.

References


About the Author

Mike Kreaden is a Partner Evangelist at salesforce.com, helping CRM application developers deliver killer commercial apps for the Force.com platform.