ASP.NET MVC 3

ネイティブとモバイルのハイブリッド Web アプリを作成する

Shane Church

コード サンプルのダウンロード

モバイル アプリを構築しようとして、使用可能なデバイスや習得すべき API が多すぎて途方に暮れたことはありませんか。いったいどのモバイル プラットフォームを選択すべきなのでしょう。Apple iOS (iPhone と iPad) は Objective C、Google Android は Java、Windows Phone は Silverlight をそれぞれ使用しています。それだけではありません。API や市場もそれぞれ異なります。特定の技術に特化してアプリを構築すると、50 パーセント、あるいはそれ以上の市場を失うことになります。すべてのプラットフォームをサポートしようとすると、少なくとも 3 つの異なるコード ベースを管理する必要が生じ、開発コストやメンテナンス コストが大幅に増大します。

別の選択肢として、これらのすべてのデバイスで表示できるという理由で、モバイル Web アプリをビルドする方法があります。しかし、このアプローチにもいくつか課題があります。HTML と JavaScript を使用してビジネス アプリケーション全体を開発する場合の最大の課題は、カメラ、GPS、加速度計など、デバイスのネイティブ ハードウェア機能の多くにアクセスできないことです。

モバイル市場が成長の道を突き進んでいることは明らかです。では、可能な限り最高のユーザー エクスペリエンスを提供しながら、このようなデバイスをすべてサポートするにはどうすればよいのでしょう。今回は、ネイティブ アプリケーション シェルでモバイル Web アプリをラップして、両方の技術を最大限活用できるモバイル アプリを構築する方法を紹介します。

ハイブリッド アプリの考え方

ハイブリッド アプリの基本的な考え方は、モバイル環境に最適化した Web アプリを、デバイス固有のネイティブ アプリケーション シェル内にラップするというものです。ネイティブ アプリケーション シェルは、Web ブラウザー コントロールをホストします。この Web ブラウザー コントロールは、シェル アプリケーションが特定のモバイル アプリを呼び出すときに、そのモバイル アプリの URL を呼び出すように構成します。ネイティブ アプリケーション シェル内では、他の UI 要素を必要に応じて提供できますが、Web ブラウザー コントロールだけは必ず提供します。このネイティブ Web ブラウザー コントロールは、ユーザーがサイトをナビゲートするときに要求される URL をリッスンします。ネイティブ機能を必要とする特定の URL をユーザーが要求すると、Web ブラウザー コントロールはナビゲーション イベントに割り込み、ネイティブ機能を呼び出します。ユーザーのネイティブ プロセスが完了すると、アプリケーションは Web ブラウザー コントロールのナビゲーション フローを、適切な場所の Web サイトのフローに戻します。

これをどのように行うかを示すため、EffectiveUI の同僚と共同で、ある顧客用にアプリを構築したときの経験について、順を追って説明します。このアプリは、モバイル現場の作業員用に構築したものです。この現場作業員は、看板、ベンチ、消火栓など、地方自治体の資産の保守作業依頼を多数扱います。このアプリでは、作業員の現在地を把握する際にはブラウザーがサポートする機能を利用し、資産の写真を撮ってサーバーにアップロードする際にはネイティブ ハードウェアのアクセス機能を利用します。図 1 は、完成したアプリのメイン メニューです。

The Completed Application Main Menu
図 1 完成したアプリケのメイン メニュー

Web アプリをビルドする

このモバイル アプリの構築時には、Steve Sanderson の記事「モバイル ブラウズ エクスペリエンスを向上する」(msdn.microsoft.com/magazine/hh288079) (MSDN Magazine、2011 年 7 月) で紹介されている多数の提案に従いました。この記事の推奨事項に加え、これまでに以下のような教訓を得ました。

  • タッチ操作向けに UI 要素を最適化する: 大半のモバイル ユーザーはタッチを主体とした操作を行います。タッチ操作は、デスクトップでマウスを使用する操作よりも本質的に正確さに欠けます。ボタンやメニュー項目などの操作要素はすべて、モバイル インターフェイスで使用する場合はデスクトップ環境よりも、一様に大きくする必要があります。
  • 帯域幅に合わせてモバイル ビューを最適化する: 特に帯域幅を考えると、大半のモバイル デバイスはリソースに制約があります。大容量の画像を多数ダウンロードしなければユーザーがサイトを使用できなくなるような状況は避けなければなりません。モバイル デバイスを使用しているユーザーは反応の良いインターフェイスを期待しており、期待通りのパフォーマンスが得られなければ、そのサイトやアプリの利用をすぐにあきらめてしまいます。
  • HTML5 と CSS3 を使用する: モバイル Web ブラウサーにはデスクトップで長年使用してきたレガシー ブラウザーがサポートされないため、デスクトップに比べて、最新の HTML5 や CSS3 標準をより迅速に導入しています。多くの場合、モバイル ブラウザーはこれらの機能を実装するという点でデスクトップ ブラウザーよりかなり優れています。モバイル ビューでもこれを活用し、モバイル ブラウザーがダウンロードする必要のあるペイロードを軽くして、ブラウザーでスタイルのレンダリングを細かく行えるようにします。

このアプリ作成時に顧客から求められた技術要件の 1 つは、サイトのデスクトップ ビューとモバイル ビューとの間で、コントローラー ロジックを共有する方法を実証することでした。この要件は多くの顧客に共通のものであり、デスクトップとモバイルの両方のユーザーをサポートするアプリの構築プロセスが大幅に簡略化されるため、開発者にとっても好ましいものです。ASP.NET MVC 3 では、複数のビュー間でコントローラーとモデルを共有しながら、ブラウザーの要求など、要求の要素に基づいてビューを切り替えることができます。また、開発者は異なるプラットフォームごとにサイトのエクスペリエンスを細かくコントロールできるため、一度ビジネス ロジックを構築するだけで、プラットフォームごとに表示を調整できるようになります。図 2 に、表示するビューを決定するユーティリティ関数を示します。

図 2 表示するビューを決定するためのユーティリティ

private ActionResult SelectView(string viewName, object model,
  string outputType = "html")
{
  if (outputType.ToLower() == "json")
  {
    return Json(model, JsonRequestBehavior.AllowGet);
  }
  else
  {
    #if MOBILE
      return View(viewName + "Mobile", model);
    #else
      if (Request.Browser.IsMobileDevice)
      {
        return View(viewName + "Mobile", model);
      }
      else
      {
        return View(viewName, model);
      }
    #endif
  }
}

このユーティリティ関数を使用すると、着信要求に基づいてユーザーに表示するビューを決定するのに同じコードを共有するという要件を満たすことができます。着信要求が HTML ではなく JSON を要求するスクリプトの場合でも、単に outputType パラメータを適切に設定するだけで、コントローラーは同じビジネス ロジックとモデル クラスを使用して適切に応答できます。また、MOBILE 条件付きコンパイル シンボルを検索するプリコンパイラ ステートメントを使用して、自分のデスクトップ ブラウザーを使用してモバイル ビューのデバッグを行えるようにもしています。これは、追加のビルド ターゲット「Mobile」を ASP.NET MVC 3 プロジェクトで使用することで実現できます。これにより、デスクトップでのデバッグ構成で Request.Browser.IsMobileDevice のチェックをスキップできるため、アプリのモバイル バージョンをビルドおよびデバッグするときに効率が大幅に高まります。

アプリをビルドするときに、サイトのモバイル バージョンとデスクトップ バージョンには別個のマスター ページを使用しました。マスター ページのデスクトップ バージョンとモバイル バージョンでは、プラットフォーム間の表示の差異に対処する方法がかなり異なります。モバイル マスター ページにはモバイル固有の CSS ファイルと簡略化したレイアウト構造を含めていて、jQuery Mobile Framework のマークアップと構文を使用する個々のビューの作成を簡略化しています。

最近のモバイル プラットフォームはすべて、デバイスの GPS 無線にアクセスし、HTML5 World Wide Web Consortium (W3C) 地理位置情報 API を介してユーザーの現在地を判断できます。地理位置情報 API の使用については、Brandon Satrom の記事「Web アプリケーションに地理位置情報を組み込む」(msdn.microsoft.com/magazine/hh580735) (2011 年 12 月) で詳しく述べられています。上記の記事では、ネイティブで HTML5 地理位置情報 API をサポートしていないブラウザー上で位置情報をサポートするために HTML5 JavaScript ポリフィルを使用することについて述べられていますが、現在のほとんどのモバイル ブラウザーは HTML5 地理位置情報 API をネイティブでサポートしているため、大部分の場合、ポリフィル技術は必要ありません。ポリフィル技術を使用する必要性を評価しながら、ターゲットにするデバイスとブラウザーを検討します。特に Android の場合は、Android エミュレーターで GPS 機能に正常にアクセスするには、地理位置情報呼び出しで enableHighAccuracy を「true」に設定しておく必要があることに注意してください (図 3 参照)。

図 3 HTML5 を使用した地理位置情報

if(navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
$("#map_canvas").GoogleMap("addMarker", {
id: "device_location",
latitude: position.coords.latitude,
longitude: position.coords.longitude,
description: "Current Location",
iconImageUrl: '@Url.Content("~/Content/images/my-location-dot.png")',
callback: function () {
$("#map_canvas").GoogleMap("panToLocation", {
latitude: position.coords.latitude,
longitude: position.coords.longitude
});
}
});
}, function (error) {
}, {
enableHighAccuracy: true
});
}

jQuery Mobile を使用する

jQuery Mobile Framework プロジェクトの Web サイト (jquerymobile.com) によると、jQuery Mobile Framework は「現在普及しているすべてのモバイル デバイス プラットフォームに対応した HTML5 ベースの統合ユーザー インターフェイス システム」です。これにはタッチ方式に適した多数のウィジェットが組み込まれており、ネイティブ モバイル アプリと同様の外観を持つモバイル Web アプリの構築作業を大幅に簡略化できます。jQuery Mobile は、NuGet パッケージ マネージャー インターフェイスを使用して NuGet を介するか、コマンド「Install-Package jquery.mobile」を実行してパッケージ マネージャー コンソールを介して、ASP.NET MVC 3 プロジェクトに追加することができます。これにより、jQuery Mobile JavaScript と CSS ファイルがプロジェクトに追加されます。ただし、依然として、jQuery Mobile JavaScript と CSS ファイルへの参照をモバイル マスター ページに追加する必要があります (図 4 参照)。

図 4 モバイル マスター ページ

<!DOCTYPE html>
<html>
<head>
  <title>@ViewBag.Title</title>
  <meta name="viewport" content="width=device-width,
    initial-scale=1.0, user-scalable=no, height=device-height" />
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
  <link href="@Url.Content("~/Content/eui_assets/css/reset.css")"
    rel="stylesheet" type="text/css" />
  <link href="@Url.Content("~/Content/jquery.mobile-1.0.min.css")"
    rel="stylesheet" type="text/css" />
  <link href="@Url.Content("~/Content/mobile.css")"
    rel="stylesheet" type="text/css" />
  <script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")"
    type="text/javascript"></script>
  @RenderSection("PreJQueryMobileInit", false)
  <script src="@Url.Content("~/Scripts/jquery.mobile-1.0.min.js")" 
    type="text/javascript"></script>
  <script type="text/javascript">
    $('a[data-ajax="false"]').live('click', function (event) {
      if (!$(this).hasClass("camera-link")) {
        $.mobile.showPageLoadingMsg();
      }
    });
  </script>
  @RenderSection("Head", false)
</head>
<body class="eui_body" id="@ViewBag.BodyID">
  @RenderBody()
</body>
</html>

jQuery Mobile では、jQuery 開発者にとって馴染み深いパターンにいくつかの重要な変更が行われます。以下は、jQuery Mobile のドキュメントからの引用です。

jQuery で最初に習得することは、DOM が読み込まれた直後にすべてが実行されるように、$(document).ready() 関数内でコードを呼び出すことです。しかし jQuery Mobile では、ユーザーがナビゲートすると [AJAX] を使用して各ページのコンテンツが DOM に読み込まれ、DOM の ready ハンドラーは最初のページに対してしか実行されません。新しいページが読み込まれて作成されると必ずコードが実行されるように、pageinit イベントにバインドすることができます。

今回は、AJAX を介してページがビューに移行されるときに地図が初期化されるように、Google Maps コントロールを含むアプリのすべてのページ内で pageinit イベントを使用しました。

モバイル マスター ページのもう 1 つの機能は @RenderSection("PreJQueryMobileInit", false) 行です (図 4 参照)。この機能を使用して、jQuery Mobile がページで初期化される前にスクリプトを実行することができます。jQuery Mobile の listview フィルター動作が完了したらカスタム コールバックをセットアップできるように、サンプル アプリではこの機能を使用して mobileinit イベントにバインドしています。また、jQuery Mobile ライブラリに 2 行のコードを追加し、filterCompleteCallback メソッドを listview プロトタイプに追加して、組み込まれたリスト フィルタリングが完了したら通知を受け取るように設定しました。これにより、フィルタリングされたリストと一致するように、地図上で一致した項目を最新状態で表示できるようになります。コールバック関数は、jQuery Mobile をいずれかのマークアップに適用する前に jQuery Mobile listview に追加する必要がありました。これは、このコードが図 5 に示す mobileinit イベント ハンドラーで実行されるためです。

図 5 mobileinit イベントへのバインド

if(navigator.geolocation) {   
  navigator.geolocation.getCurrentPosition(function (position) {
    $("#map_canvas").GoogleMap("addMarker", {
      id: "device_location",
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      description: "Current Location",
      iconImageUrl: '@Url.Content("~/Content/images/my-location-dot.png")',
      callback: function () {
        $("#map_canvas").GoogleMap("panToLocation", {
          latitude: position.coords.latitude,
          longitude: position.coords.longitude
        });
      }
    });
  }, function (error) {
  }, {
    enableHighAccuracy: true
  });
}
@section PreJQueryMobileInit {
  <script type="text/javascript">
    $(document).bind("mobileinit", function () {
      $.mobile.listview.prototype.options.filterCompleteCallback = function () {
        // Note that filtercompletecallback is a custom
        // addition to jQuery Mobile and would need to be updated
        // in future revisions.
        // See comments in jquery.mobile-1.0.js with SSC 09/12/2011
        var ids = [];
        var $visibleItems = $("#js-work-orders-list").find(
          "li:not(.ui-screen-hidden");
        for (var i = 0; i < $visibleItems.length; i++) {
          var item = $($visibleItems[i]).find("p");
          ids.push(item.text().substr(item.text().indexOf('#') + 1));
        }
        ids.push("device_location");
        $("#map_canvas").GoogleMap("hideAllMarkersExceptList", ids);
      }
    });
  </script>
}

jQuery Mobile は、header タグと footer タグ、data-* 属性など、HTML5 の新しい機能を多数活用しています。data-role 属性は、所定の要素に添付する動作を決定します。たとえば、図 6 の MapMobile.cshtml ビューでは、data-role="page" 属性を使用して 2 つの divs を 定義しています。

図 6 MapMobile.cshtml マークアップ

<div data-role="page" id="map_page" data-fullscreen="true"
  data-url="map_page" data-theme="a">
  <header data-role="header" data-position="fixed">
    <a href="@Url.Action("Index", "Home")" data-icon="home"
      data-direction="reverse">Home</a>
    <h1>Map Demo</h1>
    <a href="#" data-icon="back" id="js-exit-street-view"
      class="ui-btn-hidden">Exit Street View</a>
  </header>
  <div data-role="content" class="main-content">
    <div id="map_canvas" style="width:100%;height:100%"></div>
  </div>
  <footer data-role="footer" data-position="fixed"
    data-id="fixed-nav" data-theme="a">
    <nav data-role="navbar">
      <ul>
        <li><a href="#map_page" class="ui-btn-active
          ui-state-persist">Map</a></li>
        <li><a href="#items_page">Work Orders</a></li>
      </ul>
    </nav>
  </footer>
</div>
<div data-role="page" id="items_page" data-url="items_page" data-theme="a">
  <header data-role="header" data-position="fixed">
    <a href="@Url.Action("Index", "Home")" data-icon="home"
      data-direction="reverse">Home</a>
    <h1>Map Demo</h1>
  </header>
  <div data-role="content" class="main-content">
    <div class="list-container">
      <ul data-role="listview" id="js-work-orders-list" data-filter="true">
      @foreach (MapItem item in Model.Items)
  {
      <li class="work-order-id-@item.ID">
        <a href="@Url.Action("Details", "Home", new { id = item.ID })"
          data-ajax="false">
          <h3>@item.Issue</h3>
          <p>Work Order #@item.ID</p>
        </a>
      </li>
    }
      </ul>
    </div>
  </div>
  <footer data-role="footer" data-position="fixed"
    data-id="fixed-nav" data-theme="a">
    <nav data-role="navbar">
      <ul>
        <li><a href="#map_page" data-direction="reverse">Map</a></li>
        <li><a href="#items_page" class="ui-btn-active
          ui-state-persist">Work Orders</a></li>
      </ul>
    </nav>
  </footer>
</div>

この属性は jQuery Mobile に、これらの各 divs をモバイル デバイス上で別個のページとして扱い、ブラウザーでページ ナビゲーションを行わずに AJAX を使用してそれらの間を移行する必要があることを指示します。これにより、図 7 のスクリーンショットのようになります。jQuery Mobile Web サイトでは、jQuery Mobile コンテキストで各 data-* 属性を使用する方法についての推奨事項と詳細情報が提供されています。

Transitioning Between Pages Via AJAX
図 7 AJAX を使用したページ間の遷移

ネイティブ モバイル アプリケーション シェルをビルドする

各ネイティブ アプリケーション シェルを作成する際の基本パターンは、単に全画面表示 Web ブラウザー コントロールを含むアプリケーションを設計することです。このコントロール内では、ユーザーが新しいページを要求すると発生するイベントをキャプチャし、ネイティブ機能を呼び出す既知の URL のリストと、要求された URL を照合します。ネイティブ アプリケーション シェルでは、このコントロールで Web ベース アプリのさまざまな処理を行います。このアプリの目的では、サイト内で照合する URL は、ネイティブのカメラ機能を呼び出す "Home/Image" です。ユーザーが [Work Order] 詳細ページを開くと、図 8 に示すように、画面右上隅にカメラ アイコンが表示されます。このアイコンをクリックすると、ネイティブ カメラを呼び出します。

Invoking Native Camera Functionality
図 8 ネイティブ カメラの機能を呼び出す

Windows Phone

Windows Phone では、すべてのネイティブ機能に Silverlight を使用します。ある意味、このことから、Windows Phone は ASP.NET に精通しているモバイル Web 開発者にとって最も簡単にサポートできるプラットフォームになります。ネイティブ アプリケーション シェルの基本 XAML レイアウトは、次に示すようにシンプルです。

<Canvas x:Name="LayoutRoot" Background="Black" Margin="0">
  <phone:WebBrowser HorizontalAlignment="Left" Name="webBrowser1" 
    Navigating="webBrowser1_Navigating" IsScriptEnabled="True"
    IsGeolocationEnabled="True"
    Background="Black" Height="720" Width="480" />
</Canvas>

ここで注目すべき重要な点は、Windows Phone の Web ブラウザー コントロールはスクリプトを既定では有効にしないため、IsScriptEnabled を true に設定している点と、Navigating イベントを処理している点です。

図 9 に示す MainPage.xaml.cs で、webBrowser1_Navigating イベントを処理します。検索中の URL とナビゲーション URL が一致すると、処理中の作業依頼 の ID を選別し、Web ブラウザーのナビゲーションをキャンセルして、ネイティブの CameraCaptureタスクを呼び出します。ユーザーがカメラで写真を撮影したら、photoCaptureOrSelectionCompleted メソッドを呼び出します。ここで、撮影した写真を Web サーバーにアップロードします。このとき使用する HTTP フォームの POST アクションは、ファイルをアップロードする入力ボタンを含むフォームを送信する場合に Web サイトで使用するものと同じです。写真のアップロードが完了したら、upload_FormUploadCompleted を呼び出し、Web アプリのフローにユーザーを戻します。

図 9 Windows Phone の MainPage.xaml.cs

public partial class MainPage : PhoneApplicationPage
{
  CameraCaptureTask cameraCaptureTask;
  BitmapImage bmp;
  string id = "";
  string baseURL = "http://...";
  // Constructor
  public MainPage()
  {
    InitializeComponent();
    cameraCaptureTask = new CameraCaptureTask();
    cameraCaptureTask.Completed +=
      new EventHandler<PhotoResult>(photoCaptureOrSelectionCompleted);
  }
  private void webBrowser1_Navigating(object sender, NavigatingEventArgs e)
  {
    // Catch Navigation and launch local camera
    if (e.Uri.AbsoluteUri.ToLower().Contains("home/image"))
    {
      id = e.Uri.AbsoluteUri.Substring(e.Uri.AbsoluteUri.LastIndexOf("/") + 1);
      cameraCaptureTask.Show();
      e.Cancel = true;
    }
  }
  void photoCaptureOrSelectionCompleted(object sender, PhotoResult e)
  {
    if (e.TaskResult == TaskResult.OK)
    {
      byte[] data = new byte[e.ChosenPhoto.Length];
      e.ChosenPhoto.Read(data, 0, data.Length);
      e.ChosenPhoto.Close();
      Guid fileId = Guid.NewGuid();
      Dictionary<string, object> postParameters = new Dictionary<string, object>();
      postParameters.Add("photo", new FormUpload.FileParameter(
        data, fileId.ToString() +
        ".jpg", "image/jpeg"));
      FormUpload upload =
        new FormUpload(baseURL + "Home/UploadPicture/" + id, postParameters);
      upload.FormUploadCompleted +=
        new FormUpload.FormUploadCompletedHandler(upload_FormUploadCompleted);
      upload.BeginMultipartFormDataPost();
    }
  }
  void upload_FormUploadCompleted(object source)
  {
    webBrowser1.Navigate(webBrowser1.Source);
  }
  private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
  {
    webBrowser1.Navigate(new Uri(baseURL));
  }
}

Web ベース バージョンの Google Maps コントロールや Bing Maps コントロールを操作する場合、Windows Phone と Android や iOS とでは動作がいくつか異なります。Internet Explorer 9 モバイル ブラウザーでは、タッチ、フリック、ピンチといったジェスチャを JavaScript エンジンに渡さないで直接キャプチャしているため、Web ベースの Maps コントロールではジェスチャを使用してズームやパンを行うことができず、Maps コントロールが用意するズーム コントロールやパン コントロールを使用する必要があります。この制限を考えると、将来、このプロジェクトに、Windows Phone で地図を操作する機能を必要とするネイティブの Bing Maps コントロールを呼び出した後、地図の操作を必要としない Web アプリの画面に戻るという機能強化が必要になります。

Android

Android 用の Java コードは同僚の Sean Christmann が作成しました。このコードは Windows Phone 用のコードと似ています。次のレイアウト XML は Android WebView コントロールの全画面レイアウトを定義します。

<?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="match_parent" 
      android:layout_height="match_parent"></WebView>
</LinearLayout>

図 10 に示すように、EffectiveUIActivity.java 内では、onCreate のオーバーライドによって、WebView コントロールの onLoadResource メソッドと shouldOverrideUrlLoading メソッドをオーバーライドするように WebViewClient をセットアップします。この WebView コントロールでは、Windows Phone と同じ照合文字列を検索し、見つかったらと、カメラ アクティビティを作成してナビゲーションをキャンセルします。このコードでは onGeolocationPermissionsShowPrompt メソッドもオーバーライドします。このオーバーライドでは、アプリの実行中に WebView コントロールが GPS 位置情報へのアクセスの許可を求めるプロンプトをユーザーに表示しないようにしています。カメラ アクティビティが実行されたら、onActivityResult 関数は、前述の Windows Phone の例と同じ手法を使用して、写真を Web サーバーにポスト後、Web アプリのフローにユーザーを戻します。

図 10 Android の EffectiveUIActivity.java

    public class EffectiveUIActivity extends Activity {
      /** Called when the activity is first created. */
      WebView webView;
      String cameraId;
      static String baseURL = "http://...";
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        webView = (WebView)findViewById(R.id.webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.getSettings().setGeolocationEnabled(true);
        webView.setVerticalScrollbarOverlay(true);
        webView.loadUrl(baseURL);
        final EffectiveUIActivity activity = this;
        webView.setWebViewClient(new WebViewClient(){
          @Override
          public void onLoadResource(WebView view, String url) {
            super.onLoadResource(view, url);
            if(url.contains("Home/Image")){
              activity.createCamera();
            }
          }
          @Override
          public boolean shouldOverrideUrlLoading(WebView view, String url){
            String match = "Home/Image/";
            int i = url.indexOf(match);
            if(i>0){
              cameraId = url.substring(i+match.length());
              activity.createCamera();
              return true;
            }
            return false;
          }
        });
        webView.setWebChromeClient(new WebChromeClient(){
          @Override
          public void onGeolocationPermissionsShowPrompt(
            String origin, GeolocationPermissions.Callback callback) {
            super.onGeolocationPermissionsShowPrompt(origin, callback);
            callback.invoke(origin, true, false);
          }
        });       
      }
      public void createCamera(){
        Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
        startActivityForResult(intent, 2000);
      }
      @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
          if (resultCode == Activity.RESULT_OK && requestCode == 2000) {
            Bitmap thumbnail = (Bitmap) data.getExtras().get("data");
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            thumbnail.compress(CompressFormat.JPEG, 75, bos);
            byte[] imagebytes = bos.toByteArray();
            ByteArrayBody bab = new ByteArrayBody(imagebytes, "image/jpeg",
              UUID.nameUUIDFromBytes(imagebytes).toString()+".jpg");
            HttpClient client = new DefaultHttpClient();
            HttpPost post = new HttpPost(baseURL+"Home/UploadPicture");
            MultipartEntity reqEntity =
              new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
            reqEntity.addPart("photo", bab);
            try {
              reqEntity.addPart("ID", new StringBody(cameraId, "text/plain",
                Charset.forName( "UTF-8" )));
            } catch (UnsupportedEncodingException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
              }
              post.setEntity(reqEntity);
              try {
                HttpResponse response = client.execute(post);
                BufferedReader reader = new BufferedReader(
                  new InputStreamReader(
                  response.getEntity().getContent(), "UTF-8"));
                String sResponse;
                StringBuilder s = new StringBuilder();
                while ((sResponse = reader.readLine()) != null) {
                  s = s.append(sResponse);
                }
              } catch (ClientProtocolException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
              } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                }
                webView.loadUrl(webView.getUrl());
              }
            }
    }

iOS

iOS 用の Objective-C コードも同僚の Sean Christmann が作成しました。このコードも Windows Phone や Android に使用したコードに似ています。図 11 に示すように、WebCameraViewController.m 内で、UIWebView コントロールによって shouldStartLoadWithRequest メソッドが実行され、要求された URL とパターンの照合を行います。URL 文字列が一致すると、コードは "NO" を返してナビゲーションをキャンセルし、ネイティブの UIImagePickerController を呼び出します。この呼び出しにより、ユーザーは写真をライブラリから選択したり、搭載カメラを使用して新しい写真を撮影したりできるようになります。写真を選択後のコードでは、ASIFormDataRequest ライブラリ (allseeing-i.com/ASIHTTPRequest) を使用して写真を Web サーバーにポストバックしてから、UIWebView back を通常のアプリ フローに戻します。

図 11 iOS コード

- (void) choosefromCamera {
    UIImagePickerController *picker = [[UIImagePickerController alloc] init];
    picker.delegate = self;
    picker.mediaTypes = [NSArray arrayWithObjects:(NSString*)kUTTypeImage, nil];
    if ([UIImagePickerController
      isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
      picker.sourceType = UIImagePickerControllerSourceTypeCamera;
    }else{
      picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    }
    [self presentModalViewController:picker animated:YES];
}
- (void)imagePickerController:(UIImagePickerController *)picker   
    didFinishPickingMediaWithInfo:(NSDictionary *)info {
    UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
    NSData *jpg = UIImageJPEGRepresentation(image, 0.3);
    [picker dismissModalViewControllerAnimated:YES];
    [picker release];
    NSString *url =
      [NSString stringWithFormat:@"%@:7511/WorkOrders/UploadPicture", baseURL];
    ASIFormDataRequest *request =
      [ASIFormDataRequest requestWithURL:[NSURL URLWithString:url]];
    [request addData:jpg withFileName:[
      NSString stringWithFormat:@"%@.jpg", [self GetUUID]]
      andContentType:@"image/jpeg" forKey:@"photo"];
    [request addPostValue:cameraId forKey:@"ID"];
    [request setDelegate:self];
    [request setDidFinishSelector:@selector(imageUploaded:)];
    [request setDidFailSelector:@selector(imageUploaded:)];
    [request startSynchronous];
    [webView reload];
}
-(void) imageUploaded:(ASIFormDataRequest *)request {
    NSString *response = [request responseString];
    NSLog(@"%@",response);
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(
  NSURLRequest *)request
    navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    NSString *str = [url absoluteString];
    NSRange range = [str rangeOfString:@"WorkOrders/Image/"];
    if (range.location != NSNotFound) {
      cameraId = [str substringFromIndex:range.location+17];
      [cameraId retain];
      NSLog(@"%@", cameraId);
      [self choosefromCamera];       return NO;
    }else{
      return YES;
    }
}

モバイル エクスペリエンスのグレースフル デグラデーション

モバイル Web サイトのユーザーがカメラへのアクセスにネイティブ アプリケーション シェルを使用しないとどうなるでしょう。このようなシナリオには、ユーザー エクスペリエンスのグレースフル デグラデーションを行えることが重要です。グレースフル デグラデーションとは、最適ではないソフトウェアで表示される場合でも正しく機能を続けるようにアプリケーションをビルドするという考え方です。この考え方は、すべての機能をまったく同じように機能させようとしたり、意図するエクスペリエンスに似せようとするのではなく、ユーザーのエクスペリエンスが最高とは言えないまでも、基本的な目標をすべて実現することを目指します。

このアプリでグレースフル デグラデーションを実現するため、今回は写真撮影用 URL "Home/Image" 用に ASP.NET MVC 3 コントローラーとビューをビルドしました。ここでは、ネイティブ アプリケーション シェルによって写真を撮影し、単純なファイル アップロードフォームを提供します (図 12 参照)。このフォームは、統合されたエクスペリエンスは得られないものの、強化したモバイル シェルを使用しないユーザーでも写真を作業依頼に追加するという作業は同じように実現できます。このフォームは、ネイティブ アプリケーション シェルによって使用されているのと同じコントローラー アクションにポストするため、異なるすべてのプラットフォームやビューの間でのコードの再利用が進みます。

A Simple File Upload Form for Graceful Degradation
図 12 グレースフル デグラデーションに対応するシンプルなファイル アップロード フォーム

コスト上の大きなメリット

ハイブリッド アプリは、短期的にも長期的にも、固有のネイティブ アプリに比べて、コスト上非常に高いメリットが得られます。jQuery Mobile などのツールによって、ユーザビリティの格差を狭めることができるため、ネイティブ デバイス アクセスが不要になるという重大なビジネス上のメリットにつながる可能性があります。

モバイル デバイスの急成長に伴い、モバイル アプリをビルドする際、いくつか選択肢があります。

  • サポートするプラットフォーム単位にネイティブ アプリケーションをビルドする: この手法には、デバイスのすべてのネイティブ機能にアクセスできるようにしながら、プラットフォームごとに最高のユーザー エクスペリエンスとパフォーマンスを提供し、アプリ ストアの市場競争力を高めるという点で明らかなメリットがあります。しかし、サポートするプラットフォームごとに別のコード ベースが必要になるため、ビルドやメンテナンスの費用がかなり増加する可能性があるというデメリットがあります。また、アプリの新しいバージョンをリリースするたびに、アプリをアプリ ストアに再送信することも必要になります。
  • モバイル Web アプリをビルドする: この手法には、すべてのプラットフォームに対して、作成、実行、および更新を最も簡単かつ最小のコストで実現できるというメリットがありますが、ネイティブ ハードウェア機能にアクセスできないことから、ユーザー エクスペリエンスの点で妥協しなくてはならなくなる可能性があります。アプリ ストアにアクセスできないことも、アプリの採用が見送られる原因になる可能性があります。また、アプリについてすべてのマーケティング作業を行わなくてはならなくなります。
  • ネイティブとモバイルのハイブリッド Web アプリをビルドする: 今回の手法です。この手法では、プラットフォームごとに固有のネイティブ アプリを作成するには費用が高くつく点と、モバイル Web アプリからネイティブ ハードウェアにアクセスできない点との間で完全な妥協点を得ることができます。また、アプリ ストアにもアクセスできるため、アプリをより簡単に入手できるようになります。

重要なのは、いずれの手法も本質的に他の手法より優れているわけではなく、それぞれが独自のメリットとデメリットを持っていることです。これらの各選択肢について、コストとメリットの面で総合的な分析を行えば、顧客や会社にとっての最適な選択肢が浮かび上がってきます。この決定を行う場合は、ユーザー エクスペリエンス、先行開発コスト、運用中のメンテナンス コストに加え、マーケティングやユーザーへの導入といった軽微な要因も考慮することが重要です。

多くのビジネス アプリケーション シナリオには、モバイル Web アプリまたはハイブリッド アプリの採用をお勧めします。モバイル プラットフォームごとに固有のネイティブ アプリをビルドすると、そのための追加作業のコストがビジネス上の収益を上回る可能性があります。ビジネスのシナリオは、モバイル Web アプリまたはハイブリッド アプリのいずれかを配置するという枠組みの中で慎重に検討する必要があります。

モバイル アプリは広く普及してきており、コンピューターの使用が従来のデスクトップ エクスペリエンスから各種のモバイル エクスペリエンスにシフトする中で、ますます重要になります。モバイル環境でアプリをビルドする場合、妥協という言葉は必ずしも悪い意味で使われるわけではなく、妥協によって両方の観点から最善の策を見出せる可能性もあることを忘れないでください。

Shane Church は EffectiveUI (コロラド州デンバー) の技術リーダーです。彼は、2002 年から ASP.NET および Microsoft モバイル テクノロジに重点を置いた Microsoft .NET Framework の開発を行っています。彼のブログは s-church.net (英語) で公開されています。EffectiveUI については、effectiveui.com (英語) を参照してください。

この記事のレビューに協力してくれた技術スタッフの James McCaffrey 博士に心より感謝いたします。