Creating a sample Hybrid Mobile Application
Now that we’ve configured the web server for a mobile-specific sub-domain, and configured eZ Publish to support a mobile siteaccess and mobile design, we can start to build the design, the mobile site as a whole, and subsequently the hybrid mobile application -- one for the iOS / iPhone and one for Android phones -- as a shell for the mobile site!
Here is a screenshot of the iOS application that will be the result of this tutorial:
Preparation
The first thing we will need is a working eZ Publish instance with the Website Interface enabled. We won’t cover the steps for how to install eZ Publish, as this process is well documented in the official documentation. Refer to the "Normal installation" section of the "Installation" chapter for more information. We also assume that you have already created a siteaccess and design extension dedicated to the mobile site, as covered earlier in this article.
Adjusting INI settings
We will be using the Website Interface design as the foundation for our mobile web channel, so we need to set the “ezwebin” design as a fallback site design in order to use its templates, styles, and so on. Verify that the configuration file settings/siteaccess/mobile/site.ini.append.php has the following settings:
[DesignSettings] SiteDesign=mobile AdditionalSiteDesignList[] AdditionalSiteDesignList[]=ezwebin AdditionalSiteDesignList[]=base
A bit later in this article, we will add a "pagelayout-mobile.css" file, which contains CSS styles for the main template "pagelayout.tpl". In preparation for that, we will add a reference to that file in extension/mobile/settings/design.ini.append.php:
[StylesheetSettings] FrontendCSSFileList[]=pagelayout-mobile.css
To complete our work with INI settings, we also need to copy over the Website Interface image size settings, as well as template override rules so that our mobile site can make use of much of the Website Interface foundation. Copy the following files from settings/siteaccess/ezwebin_site/ to settings/siteaccess/mobile/
1. "override.ini.append.php"
2. "image.ini.append.php"
Preparing templates
Next, copy the following templates from the extension/ezwebin/design/ezwebin/ folder to extension/mobile/design/mobile/. We will be modifying these templates instead of simply using them as fallback templates.
1." templates/pagelayout.tpl"
2. "templates/page_header.tpl"
3. "templates/page_header_logo.tpl"
4. "templates/page_head.tpl"
5. "templates/content/search.tpl"
6. override/templates/full/article.tpl"
7. override/templates/full/folder.tpl"
8. override/templates/full/frontpage.tpl"
Once you are done copying the files, your mobile extension’s design folder structure should look like this:
mobile
│
├── design
│ └── mobile
│ ├── images
│ ├── javascript
│ ├── override
│ │ └── templates
│ │ └── full
│ │ ├── article.tpl
│ │ ├── folder.tpl
│ │ └── frontpage.tpl
│ ├── stylesheets
│ └── templates
│ ├── content
│ │ └── search.tpl
│ ├── page_head.tpl
│ ├── page_header.tpl
│ ├── page_header_logo.tpl
│ └── pagelayout.tpl
└── settings
Here are the changes to make to the newly copied templates:
pagelayout.tpl
Make the following changes to extension/mobile/design/mobile/templates/pagelayout.tpl:
The following code change will scroll up to the device’s viewport and hide the browser navigation toolbar. Change line 29 from :
<!--[if (gt IE 8)|!(IE)]><!--><body><!--<![endif]-->
to
<!--[if (gt IE 8)|!(IE)]><!--><body onload="{literal}setTimeout(function() { window.scrollTo(0, 1) }, 100);{/literal}"><!--<![endif]-->"
Line 33 should already read:
<div id="page" class="{$pagestyle}">
Add to the following line ( line 34 ), which is a “div” element to which we will add a background image style later:
<div id="page-wrapper">
Line 100 should already read:
{include uri='design:page_footer.tpl'}
Add to the following line ( line 101 ) to close the element added above:
</div>
page_header.tpl
Make the following changes to extension/mobile/design/mobile/templates/page_header.tpl:
Replace line 9 with the following code, for styling purposes and to add a link on all sub-pages, pointing back to the home page:
<div class="grid"> <div class="unit-1-4"> {def $current_node = fetch( 'content', 'node', hash( node_id, $current_node_id ))} {if and( ne( $current_node_id, ezini( 'NodeSettings', 'RootNode', 'content.ini' ) ), $current_node)} <div id="backbutton"> <a href="{$current_node.parent.url_alias|ezurl( 'no' ))}"><img src="{'back.png'|ezimage( 'no' )}" /></a> </div> {/if} {undef $current_node} </div> <div class="unit-3-4"> {include uri='design:page_header_logo.tpl'} </div> </div>
page_header_logo.tpl
Make the following changes to extension/mobile/design/mobile/templates/page_header_logo.tpl: Replace the contents of the entire file with the following, to simplify the code displaying the logo:
<div id="logo"> <a href={"/"|ezurl} title="{ezini('SiteSettings','SiteName')|wash}"><img src="{'logo.png'|ezimage( 'no' )}" alt="{ezini('SiteSettings','SiteName')|wash}" /></a> </div>
page_head.tpl
Make the following changes to extension/mobile/design/mobile/templates/page_head.tpl: Add the following to line 52, in order to force the webpage into the device’s visible area:
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximumscale= 1.0; minimum-scale=1.0; user-scalable=yes" />
search.tpl
Make the following changes to extension/mobile/design/mobile/templates/content/search.tpl: Remove the following code from lines 34 - 37 (referencing the advanced search functionality, which we won’t use for the mobile site):
{let adv_url=concat('/content/advancedsearch/',$search_text|count_chars()|gt(0)| choose('',concat('?SearchText=',$search_text|urlencode)))|ezurl} <label>{"For more options try the %1Advanced search%2"| i18n("design/ezwebin/content/search","The parameters are link start and end tags.",array(concat("<a href=",$adv_url,">"),"</a>"))}</label> {/let}
Wrap the code on lines 47 - 49 with the following “if” statement, which suppresses a simple message if the user hasn’t yet searched for anything:
{if ne( $search_text, '')} <div class="warning"> <h2>{'No results were found when searching for "%1".'| i18n("design/ezwebin/content/search",,array($search_text|wash))}</h2> </div> {/if}
article.tpl
Make the following changes to extension/mobile/design/mobile/override/templates/full/article.tpl:
Add the following code to line 3, to disable extra navigational and design elements from appearing in the pagelayout:
{set scope=global persistent_variable=hash('left_menu', false(), 'extra_menu', false(), 'show_path', false(), 'top_menu', false())}
To have article images display in a more mobile-friendly size, change line 32 from:
{attribute_view_gui attribute=$node.data_map.image image_class=medium}
to
{attribute_view_gui attribute=$node.data_map.image image_class=articleimage}
Related to the reason above, change line 35 from:
<div class="caption" style="width: {$node.data_map.image.content.medium.width}px">
to
<div class="caption" style="width: {$node.data_map.image.content.articleimage.width}px">
folder.tpl
Make the following changes to extension/mobile/design/mobile/override/templates/full/folder.tpl: Add the following code to line 3, to disable extra navigational and design elements from appearing in the pagelayout:
{set scope=global persistent_variable=hash('left_menu', false(), 'extra_menu', false(), 'show_path', false(), 'top_menu', false())}
frontpage.tpl
Make the following changes to extension/mobile/design/mobile/override/templates/full/frontpage.tpl: Replace the entire content of the file with the following, which disables extra navigational and design elements from appearing in the pagelayout, and sets up the basic design for the home page:
{set scope=global persistent_variable=hash('left_menu', false(), 'extra_menu', false(), 'show_path', false(), 'top_menu', false())} <div class="grid"> <div class="unit-1-2"> <div class="category"> <a href="{'/Recipes'|ezurl( 'no' )}"><img src="{'recipes.png'|ezimage( 'no' )}" /></a> <h2>{'Recipes'|i18n('design/mobile/frontpage')}</h2> </div> </div> <div class="unit-1-2"> <div class="category"> <a href="{'/Food-reviews'|ezurl( 'no' )}"><img src="{'food-reviews.png'| ezimage( 'no' )}" /></a> <h2>{'Food reviews'|i18n('design/mobile/frontpage')}</h2> </div> </div> </div> <div class="grid"> <div class="unit-1-2"> <div class="category"> <a href="{'/Menus'|ezurl( 'no' )}"><img src="{'menus.png'|ezimage( 'no' )}" /></a> <h2>{'Menus'|i18n('design/mobile/frontpage')}</h2> </div> </div> <div class="unit-1-2"> <div class="category"> <a href="{'/content/search'|ezurl( 'no' )}"><img src="{'search.png'| ezimage( 'no' )}" /></a> <h2>{'Search'|i18n('design/mobile/frontpage')}</h2> </div> </div> </div>
Images
We have already prepared images for our mobile website, which you can download here [https://github.com/ezsystems/ezmobile/tree/master/ezpublish/extension/mobile]. Copy the following files into the extension/mobile/design/mobile/images folder:
1. back.png
2. bg.png
3. food-review.png
4. footer-bg.png
5. header-bg.png
6. logo.png
7. menus.png
8. recipes.png
9. search.png
Stylesheets
In the previous steps, we have made the necessary template modifications and added images used in the mobile site design. Next, we are going to create two CSS files, one that is responsible for styling the pagelayout template and another that is responsible for styling actual content templates. Create these CSS files in extension/mobile/design/mobile/stylesheets/ as follows:
mobile
│
├── design
│ └── mobile
│ ├── images
│ ├── javascript
│ ├── override
│ │ └── templates
│ │ └── full
│ ├── stylesheets
│ │ ├── content.css
│ │ └── pagelayout-mobile.css
│ └── templates
│ ├── content
└── settings
pagelayout-mobile.css
In extension/mobile/design/mobile/stylesheets/pagelayout-mobile.css, paste the following:
body { background-position: top center; background-image: url(../images/bg.png); background-repeat: repeat-y; color: #003F72; } div#page { width: 320px; background-image: url(../images/header-bg.png); background-repeat: no-repeat; } div#page-wrapper { background-image: url(../images/footer-bg.png); background-position: bottom left; background-repeat: no-repeat; } div#header { height: 122px; background-color: transparent; padding-bottom: 0; padding-top: 0; } div#logo { margin: 0; padding-left: 3%; } div#backbutton { padding-left: 17px; padding-top: 21px; } div#usermenu, div#searchbox, div#topmenu-position, div#path { display: none; } div#footer { height: 80px; background-color: transparent; background-image: none; padding: 0; margin: 0; } div#footer address { padding: 5.25em 5em 0 5em; color: #FFF; font-size: 90%; } div#footer address a { color: #FFF; }
content.css
In extension/mobile/design/mobile/stylesheets/content.css, paste the following:
div.grid { letter-spacing: -0.31em; *letter-spacing: normal; word-spacing: -0.43em; } div.unit-1-2, div.unit-1-4, div.unit-3-4 { display: inline-block; zoom: 1; *display: inline; letter-spacing: normal; word-spacing: normal; vertical-align: top; } div.unit-1-2 { width: 50%; } div.unit-1-4 { width: 25%; } div.unit-3-4 { width: 75%; } div.category { text-align: center; } div.category h2 { font-weight: normal; color: #003F72; } div.content-view-full, div.content-search, div.content-edit { padding: 1em 1em 2.5em 1em; } div.attribute-header h1 { color: #003F72; font-size: 1.8em; } div.content-view-line div.class-article { border-bottom: 1px dotted #003F72; margin-bottom: 0.5em; } div.content-view-line div.class-article h2 a { color: #990000; font-weight: normal; } div.content-view-line div.attribute-image { float: left; margin: 0.25em 0.25em 0 0; } div.content-search div.feedback h2 { color: #003F72; font-weight: normal; } div.content-search div.feedback { padding-top: 0.5em; border-top: 1px dotted #003F72; margin-bottom: 2em; margin-top: 1em; } div.content-search input.halfbox { border: 1px solid #003F72; background-color: #98CDF1; width: 65%; height: 21px; } input.box { border: 1px solid #003F72; background-color: #98CDF1; height: 21px; } textarea.box { border: 1px solid #003F72; background-color: #98CDF1; } div.content-search input.button { font-size: 110%; } div.content-view-full div.content-view-children { border-top: 1px dotted #003F72; margin-top: 0.5em; padding-top: 0.5em; } div.content-view-line div.class-comment h2 { font-weight: normal; color: #990000; } div.attribute-star-rating { display: none; } div.attribute-byline p.date { float: left; font-size: 80%; color: #006DC6; } div.attribute-byline p.author { float: right; font-size: 80%; color: #006DC6; } input.button, input.defaultbutton { color: #FFDDDD; border: 1px solid #990000; background-color: #990000; background-image: url(../images/button-bg.png); background-repeat: repeat-x; font-size: 85%; padding: 0.15em 1em; font-weight: normal; cursor: pointer; margin: 0.5em 0; } div.content-edit label { color: #000000; } div.content-edit div.attribute-header h1 { color: #990000; }
Content
All that’s left to complete the mobile siteaccess is to populate some content. Log in to the Administration Interface and create three objects of the Folder content class, placing them beneath the root of the site. They should have the following titles: “Recipes”, “Food reviews”, and “Menus”. If you access your mobile siteaccess’ URL in a mobile browser, you should see the following:
Mobile applications
In this section, we are going to build two simple mobile applications, one for iOS / iPhone and one for the Android platform. These applications will simply be native shells for our mobile web channel. We will not cover the steps for how to set up iOS and Android development environments, as those are well documented in the official guides. Refer to the “iOS Developer Tools” or “Android SDK Installation Guide” for more information. We assume that you already have a development environment for at least one platform.
iOS application
Open Xcode, and on the very first screen, select “Create a New Xcode project”. Or, select “New Project...” from the “File” menu.
A window will appear giving you several application templates to choose from. Create a barebones Cocoa Touch application by selecting the “Window-Based Application” icon.
Click the “Next” button. On the next screen, type “DemoApp” for the product name, and “com.ez” for the company identifier. Select “iPhone” from the “Device Family” drop-down list and unmark the “Include Unit Tests” check-box (as we are not going to write unit tests this time).
Click the “Next” button and select the location at which you want to save the project. Once the project is created, the project window will appear on your screen. Take a look at the contents of the “Project” table on the left side of the project window. In general, there are two types of files used to create an application: code and resources. Code is written in Objective-C, C, or C++. Resources are things like images and sounds that are used by the application at run-time. The groups in the project window are purely for organizing files. You can rename them to whatever you want. In the next step, we are going to create a new view controller that will be responsible for loading our web view with content served by eZ Publish. Select the “DemoApp” group on the left side and select “New -> New File...” from the “File” menu.
Then, select the “UIViewController” subclass from the “Cocoa Touch” group and click the “Next” button.
On the next screen, verify that the “Subclass of” field contains the “UIViewController” class, and that the checkbox titled “With XIB for user interface” is marked. (XIB stands for XML Interface Builder, which is covered shortly.) Then, click the “Next” button and name the file “MainController”.
Next, we need to load “MainController”, which at this point does nothing. However we will add a “UIWebView” control later, which is responsible for loading our web content. Select “DemoAppAppDelegate.h” and insert the following code:
#import "MainController.h"
Next, in “DemoAppAppDelegate.m”, create the main view controller and set it as the “rootViewController” of the window:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions { // Create the mainController MainController *mainController = [[MainController alloc] init]; // Set mainController as rootViewController of window [self.window setRootViewController:mainController]; // The window retains mainController, we can release our reference [mainController release]; // Show the window [self.window makeKeyAndVisible]; return YES; }
Build and run the application to verify that everything works fine. At this point, you should see only a blank white window. Next, we are going to add the web view responsible for loading the content served by eZ Publish. Because we will need to access that web subview during runtime, “MainController” needs an outlet for that subview. Add the following instance variable to “MainController.h”:
@interface MainController : UIViewController { IBOutlet UIWebView *webView; }
The “IBOutlet” reference in front of the instance variable indicates that you are going to use the Interface Builder to lay out the interface for MainController’s view. When we created MainController, an XIB (XML Interface Builder) file of the same name was created and added to the project. Open MainController.xib now.
In the right pane, find the “UIWebView” control in the “Object” library and drag it into the view.
Next, make a connection from “File’s Owner” to that object, as shown below:
After a “UIViewController” loads its view, it is immediately sent the message “viewDidLoad”. Whether that view is loaded from an XIB file or using the method “loadView”, this message gets sent to the view controller.
We need to do an extra initialization to a “UIViewController” -- which requires its view to already exist -- by overriding “viewDidLoad” in “MainController.m”.
- (void)viewDidLoad { //Create a URL object NSURL *url = [NSURL URLWithString:@"http://m.example.com"]; //Request the URL NSURLRequest *request = [NSURLRequest requestWithURL:url]; [webView loadRequest:request]; }
Notice that “NSURL URLWithString” takes a URL, which points to our mobile web channel available at http://m.example.com. When “MainController’s” view gets unloaded, its subviews will still be retained by “MainController”. They need to be released and set to “nil” in “viewDidUnload”. Let’s override this method in “MainController.m”:
- (void)viewDidUnload { [super viewDidUnload]; [webView release]; webView = nil; }
Finally, we need a “dealloc” method, which frees up the object’s memory and disposes of any resources it holds, including ownership of any object instance variables:
- (void)dealloc { [webView release]; [super dealloc]; }
That’s all! Build and run the application to verify that everything works fine.
Android application
Open Eclipse and select “New -> Android Project” from the “File” menu.
You can then enter some details for the new project. Enter “DemoApp” as the project name, select “Android 2.3.3” from the “Build Target” list, enter “DemoApp” as the application name, and enter “com.ez” as the package name. Make sure that the checkbox “Create Activity” is marked (an activity is the application’s presentation layer), enter “DemoApp” in the adjacent field, and enter “10” as the minimum SDK version. Finally, click the “Finish” button.
Eclipse will then create your project. You can inspect the project structure in the left panel.
Next, select “Window -> Android SDK and AVD Manager”. In the resulting dialog, select “Virtual Devices” from the left panel and click the “New…” button.
Enter a name for your device, and choose an SDK target and screen resolution. Set the SD Card size to 12MiB (this is just to specify a card size for the Android emulator).
In this example, we want to present users with our eZ Publish mobile siteaccess. The preferred method for creating the UI for our Android app is to use a layout resource file. Open the “main.xml” layout file in the res/layout project folder. Modify the main layout to include a “WebView” with a “LinearLayout”. It is important to give the “WebView” an ID, so that we can reference it later in our code.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <WebView android:id="@+id/webview" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
With our user interface defined, open the “DemoApp” activity ( “DemoApp.java” ) from the project’s source folder. We will make all changes by overriding the “onCreate” method. Start by inflating your UI using “setContentView”. Then you can reference the “WebView” that was defined earlier by using findViewById().
package com.ez; import android.app.Activity; import android.os.Bundle; import android.view.KeyEvent; import android.webkit.WebView; public class DemoApp extends Activity { WebView webview; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); webview = (WebView) findViewById(R.id.webview); webview.setWebViewClient(new DemoAppWebViewClient()); webview.getSettings().setJavaScriptEnabled(true); webview.loadUrl("http://m.example.com"); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK) && webview.canGoBack()) { webview.goBack(); return true; } return super.onKeyDown(keyCode, event); } }
Notice that we have loaded the URL “http://m.example.com” into the “WebView”. In addition, we defined a listener “onKeyDown” that will perform the familiar goBack() action when the device’s “Back” button is pressed.
Next, we need to create a custom class “DemoAppWebViewClient” that extends “WebViewClient” and overrides the method “shouldOverrideUrlLoading()”. Select “New -> Class” from the “File” menu in the “src/com.ez” package. Then, enter “DemoAppWebViewClient” as the name for the class.
Next, click the “Browse” button to the right of the “Superclass” field. In the resulting dialog, type “android.webkit.WebViewClient”, then click OK. To complete the new class creation, click the “Finish” button.
The overridden “shouldOverrideUrlLoading()” method should be implemented as follows:
package com.ez; import android.webkit.WebView; import android.webkit.WebViewClient; class DemoAppWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } }
In the next step, we are going to hide the Android application title bar so that our application can work full screen. In order to do so, add the following attribute into the “DemoApp” activity in “AndroidManifest.xml”:
android:theme="@android:style/Theme.NoTitleBar"
This is what the complete “AndroidManifest.xml” file should look like:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.ez" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".DemoApp" android:theme="@android:style/Theme.NoTitleBar" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <uses-sdk android:minSdkVersion="9" /> <uses-permission android:name="android.permission.INTERNET" /> </manifest>
Run or debug the application and you should see your mobile web channel in an Android application!