Windows Phone

C# と Xamarin を使ってクロスプラットフォーム型のモバイル ゴルフ アプリをビルドする

Wallace B. McClure

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

ゴルフ シーズン到来の楽しみの 1 つは、ドライビング コンテストなどのイベントを呼び物にするトーナメントへの参加です。ドライビング コンテストでは、指定ホールの第 1 打の飛距離を他の参加者と競います。当日の最長飛距離を記録した人が優勝者です。ただし、このようなコンテストでは参加者の飛距離が一元管理されていないのが一般的です。先頭グループに入ってしまうと、イベントが終わり、他の参加者全員の記録が出揃うまで順位がわかりません。そこで、携帯電話を使ってティー ショットの位置と、ボールの落下地点を記録し、その情報をクラウド ホスト型データベースに保存してみてはどうでしょう。

このようなアプリをビルドする際のオプションはたくさんあり、困惑するかもしれません。今回はこのアプリを Windows Azure のバックエンド オプションを使ってビルドしてみました。この記事では、このアプリのビルド方法と、ビルド過程で発生するさまざまな問題への対処方法についてのチュートリアルを提供します。今回は Windows Phone 向けのアプリを作成するためのコードと、Xamarin を使用して iOS 向けのアプリを作成するためのコードを示します。

いくつか特徴となる要件を決めました。まず、アプリが複数のモバイル デバイスと複数のデバイス OS で動作するようにします。どのデバイスでも、そのデバイスの他のすべてのアプリと同じような見た目のネイティブ アプリになるようにします。バックエンド サーバーは常時利用可能で、開発者 (自分) にかかる手間を最小限に抑えます。クラウド サービスとしては、クロスプラットフォーム開発にできる限り多くの支援を提供するものを選び、ある程度の位置情報機能を提供するバックエンド データベースを使用します。

C# にする理由

モバイル Web アプリにする、iPhone (および Android) 向け Xamarin C# を使用する、クロスプラットフォーム開発をあきらめベンダー指定の言語 (Windows Phone であれば Microsoft .NET Framework、iPhone であれば Objective-C) でアプリをビルドするなど、クラスプラットフォーム アプリをビルドするオプションはいくつかあります。まず、筆者自身の好みでクライアント言語に C#/.NET Framework を選びました。C# にすれば、デバイスのプラットフォーム固有の特徴を学んだ後、さらに言語を学習する時間が必要になりません。iPhone 向けに Xamarin ソリューションを選んだのは、Visual Studio 2013 ですべてを開発できることが大きな理由です。モバイル Web アプリというオプションを選んだ場合の問題は、ユーザーが可能な限りプラットフォームと密接に統合されたアプリを求めることです。このような密接な統合はモバイル Web アプリというソリューションでは難しくなりますが、ネイティブ ソリューションを使えば簡単です。クロスプラットフォーム開発をあきらめ、ベンダー指定のソリューションを選ぶと、プラットフォームごとに新しい言語を学ばなくてはならないので合理的ではありません。

開発ツール

複数のプラットフォーム向けにアプリをビルドする場合、一般的には複数の開発ツールが必要になります。Xamarin.iOS が登場するまでは、iPhone 向けの開発と言えば Xamarin Studio (以前はオープン ソースの SharpDevelop を移植した MonoDevelop) を Mac 上で使うしかありませんでした。Xamarin Studio 自体に問題があるわけではなく、開発者が一般的にできる限り 1 つの IDE で作業したいと望むことが問題です。Visual Studio 2013 と Visual Studio 用の Xamarin.iOS とを併用すれば、お気に入りの IDE だけで、Windows Azure、Windows Phone、および iPhone 向けの開発が可能です。

Windows Azure

モバイル デバイスから Windows Azure にアクセスする方法はいくつかあります。たとえば、Windows Azure バーチャル マシン (VM)、Web ロール、Windows Azure Web サイト、Windows Azure のモバイル サービス (WAMS) などです。

VM を使えば、プラットフォームの全バリエーションをほぼ制御でき、アプリとすべてのサーバー設定に変更を加えることができます。これは、基盤となる OS 設定のカスタマイズや別のアプリのインストール、または他にも変更の可能性があるようなアプリには優れた選択肢です。これをサービスとしてのインフラストラクチャ (IaaS: Infrastructure as a Service) と呼びます。

Windows Azure には一連のロールが組み込まれたクラウド サービスというプロジェクトの種類があります。ロールは、Visual Studio のプロジェクトにほぼ対応します。Web ロールは基本的には Web プロジェクトで、VM 内にアップロードされ実行される 1 つの配置可能なパッケージにバンドルされます。Web ロールは Web UI を提供し、内部で Web サービスを利用することも可能です。ワーカー ロールはサーバー側で連続実行されるプロジェクトです。これをサービスとしてのプラットフォーム (PaaS: Platform as a Service) と呼びます。

Windows Azure Web サイトは Web ロールと考え方が似ています。このソリューションは、アプリが Web サイトやプロジェクトをホストできるようにします。プロジェクトには Web サービスを含むことができます。プロジェクトに含まれる Web サービスは、SOAP 呼び出しや REST 呼び出しを利用して呼び出すことができます。Windows Azure Web サイトは、アプリが IIS を必要とするときのみ優れたソリューションです。

最初の 3 つのオプションは、Web サービスについての完璧な知識が求められます。Web サービスの呼び出し方法、データベースへの保存方法、モバイル デバイスとクラウドとの接続方法などを知っておく必要があります。マイクロソフトには、データをクラウドに迅速かつ容易に保存し、プッシュ通知を処理して、ユーザーを迅速かつ容易に認証できるソリューションがあります。それが WAMS です。他にもソリューションをビルドするオプションがありますが、今回はコーディング量を最小限に抑え、クロスプラットフォームに対する優れたサポートを考えると、WAMS を使用するのが賢明だと判断しました。WAMS を簡単に表すとすれば、「ビルドする必要のないバックエンド」でしょうか。

Windows Azure モバイル サービス (WAMS)

WAMS にはストレージ、複数のソーシャル ネットワークに対するユーザー認証、(Node.js により) サーバー側のロジックを作成するメカニズム、プッシュ通知、およびデータに対する作成、読み取り、更新、削除 (CRUD) 操作を容易にするパッケージ化されたクライアント側コード ライブラリが備わっているため、モバイル開発作業が簡単になります。

WAMS を使用する最初の手順は、モバイル サービスと関連データ ベース テーブルの作成です。厳密には、データベース テーブルは必要ありません。セットアップ プロセスの詳細については、bit.ly/Nc8rWX (英語) の Windows Azure チュートリアルを参照してください。

サーバー スクリプト: WAMS では、基本的な CRUD 操作はサーバー側の操作を通じて利用できます。各操作は delete.js、insert.js、read.js、および update.js の各ファイルに収められ、サーバー側で Node.js を使用して処理されます。WAMS における Node.js の詳細については、「Work with server scripts in Mobile Services」(英語) を参照してください。

まず、図 1 の insert.js ファイルを見ていきます。メソッドのシグネチャの "item" パラメーターにデータが渡されます。オブジェクトのメンバーは、クライアントから渡されるデータ オブジェクトに対応します。クライアントからのデータを設定するセクションを先に見た方がわかりやすいかもしれません。"user" パラメーターには、接続しているユーザーに関する情報が含まれます。今回の例では、このユーザーの認証を行う必要があります。アプリでは認証に Facebook と Twitter を使用するため、返されるユーザー ID は "Network:12345678" という形式です。値の "Network" 部分には、ネットワーク プロバイダーの名前を含みます。今回の例では、Facebook または Twitter のいずれかを利用できるため、どちらかが値に含まれることになります。数値 "12345678" は、単なるユーザー ID です。今回の例では Twitter と Facebook を使用しますが、Windows Azure ではマイクロソフトや Google のアカウントも使用できます。

図 1 ゴルフのドライバー ショットのデータをクラウド上のデータベースに挿入するときに使用する Insert.js ファイル

 

function insert(item, user, request) {
  if ((!isNaN(item.StartingLat)) && (!isNaN(item.StartingLon)) &&
    (!isNaN(item.EndingLat)) && (!isNaN(item.EndingLon))) {
    var distance1 = 0.0;
    var distance2 = 0.0;
    var sd = item.StartingTime;
    var ed = item.EndingTime;
    var sdate = new Date(sd);
    var edate = new Date(ed);
    var res = user.userId.split(":");
    var provider = res[0].replace("'", "''");
    var userId = res[1].replace("'", "''");
    var insertStartingDate = sdate.getFullYear() + "-" +
       (sdate.getMonth() + 1) + "-" + sdate.getDate() + " " +
      sdate.getHours() + ":" + sdate.getMinutes() + ":" +
      sdate.getSeconds();
    var insertEndingDate = edate.getFullYear() + "-" +
      (edate.getMonth() + 1) + "-" + edate.getDate() + " " +
      edate.getHours() + ":" + edate.getMinutes() + ":" + 
      edate.getSeconds();
    var lat1 = item.StartingLat;
    var lon1 = item.StartingLon;
    var lat2 = item.EndingLat;
    var lon2 = item.EndingLon;
    var sp = "'POINT(" + item.StartingLon + " " + 
      item.StartingLat + ")'";
    var ep = "'POINT(" + item.EndingLon + " " + 
      item.EndingLat + ")'";
    var sql = "select Max(Distance) as LongDrive from Drive";
    mssql.query(sql, [], {
      success: function (results) {
        if ( results.length == 1)
        {
          distance1 = results[0].LongDrive;
        }
      }
    });
    var sqlDis = "select [dbo].[CalculateDistanceViaLatLon](?, ?, ?, ?)";
    var args = [lat1, lon1, lat2, lon2];
    mssql.query(sqlDis, args, {
      success: function (distance) {
        distance2 = distance[0].Column0;
      }
    });
    var queryString = 
      "INSERT INTO DRIVE (STARTINGPOINT, ENDINGPOINT, " +
      "STARTINGTIME, ENDINGTIME, Provider, UserID, " +
      "deviceType, deviceToken, chanelUri) VALUES " +
      "(geography::STPointFromText(" + sp + ", 4326), " +
      " geography::STPointFromText(" + ep + ", 4326), " +
      " '" + insertStartingDate + "', '" +
      insertEndingDate + "', '" + provider + "', " + userId + ", " +
      item.deviceType + ", '" + item.deviceToken.replace("'", "''") +
       "', " + "'" + item.ChannelUri.replace("'", "''") + "')";
    console.log(queryString);
    mssql.query(queryString, [], {
      success: function () {
        if (distance2 > distance1) {
          if (item.deviceType == 0) {
            push.mpns.sendFlipTile(item.ChannelUri, {
              title: "New long drive leader"
            }, {
                  success: function (pushResponse) {
                    console.log("Sent push:", pushResponse);
                  }
               });
            }
          if (item.deviceType == 1) {
            push.apns.send(item.deviceToken, {
              alert: "New Long Drive",
              payload: {
                inAppMessage: "Hey, there is now a new long drive."
              }
            });
          }
        }
      },
      error: function (err) {
        console.log("Error: " + err);
      }
    });
    request.respond(200, {});
  }
}

最初に行うのは、入力を検証するコードのテストです。今回は、提供される経度と緯度が有効な数値であることを検証します。有効な数値でなければ挿入は直ちに終了します。次の手順では、渡されたユーザー ID を解析して、ネットワーク プロバイダーと数値形式のユーザー ID を取得します。3 番目の手順では、日付をセットアップして、データベースに挿入できるようにします。JavaScript と SQL Server とでは、日付と時刻の表現が異なっているため、これらを解析して適切な形式に変換します。

ここで、クエリを実行します。CRUD ステートメントを実行する Node.js コマンドは、mssql.query(command, parameters, callbacks) を呼び出します。"command" パラメーターは、実行する SQL コマンドです。"parameters" パラメーターは JavaScript 配列で、実行するコマンドで指定するパラメーターに対応します。"callbacks" パラメーターには、成功か失敗かに応じてクエリ完了時に使用される JavaScript コールバックを含みます。初期クエリが成功した場合のコンテンツについては、プッシュ通知に関するセクションで説明します。

最後はデバッグの問題です。スクリプトで起きていることを把握するにはどうすればよいでしょう。JavaScript に console.log(info) メソッドがあります。"info" パラメーターを指定してこのメソッドを呼び出すと、パラメータの内容がサービスのログ ファイル内に保存されます (図 2 参照)。画面右上の組み込みの更新 (refresh) 機能に注目してください。

Log File Information in Visual Studio 2013
図 2 Visual Studio 2013 のログ ファイル情報

WAMS をセットアップしたら、windowsazure.com ポータルまたは Visual Studio を使ってサービスを管理します。

注 : WAMS スクリプト ファイルからメソッドを呼び出すと、両者は異なるスキーマで実行されるため、既定のセットアップではエラーになるおそれがあります。状況によっては、アクセス許可を付与する必要があります。この問題については、Jeff Sanders のブログ記事 (bit.ly/1cHQ4Cu、英語) を参照してください。

スケール

モバイル アプリはインフラストラクチャに大きな負荷をかけることがありますが、さいわい Windows Azure にはこれに対処するオプションが複数あります。まず、メッセージ キューに関して選択できる手法がいくつかあります。

Windows Azure では、サービス バスおよび Windows Azure キュー サービスを使ってキューを利用できます。キューを使うと、データをアプリと関連付けずにすばやく保存できます。負荷が高いときは、アプリがリモート データ ソースからの応答を待機することになりますが、アプリはデータ ソースと直接やり取りしないでキューにデータを保存して、処理を続行できます。個人的な経験では、キューを使用するとアプリのスケーラビリティを容易に向上できます。今回のアプリではキューを使用しませんが、操作の負荷とシステムにアクセスするモバイル デバイス数に応じたオプションとして理解しておいてください。さいわい、サービス バスと Windows Azure キュー サービスはどちらも、WAMS のサーバー スクリプトからキューにアクセスするために必要な API が備わっています。

全体として、キューはデータを集中的に扱うアプリには優れたソリューションです。スケールに関するもう 1 つのツールは自動スケールです。Windows Azure では、アプリの正常性と可用性をダッシュボードから監視できます。サービスの可用性が低下したときに、アプリの管理者に通知するルールをセットアップできます。Windows Azure により、アプリは需要に合わせてスケール アップまたはスケール ダウンされます。既定では、この機能は無効になっています。この機能を有効にすると、Windows Azure はサービスの API 呼び出し数を定期的にチェックし、呼び出し数が API クォータの 90% を超えるとスケール アップが行われます。日が変わるとスケール ダウンが行われ、最小値に戻されます。一般には、1 日の予測トラフィックを処理できる 1 日あたりのクォータを設定し、必要に応じて Windows Azure がスケール アップを行えるようにします。本稿執筆時点では、正常性、監視、および自動スケーリングはプレビューで利用可能です。

データベース

データはあらゆるアプリの根幹をなし、ほぼすべてのビジネスにとっての基盤となります。データベースとしては、サードパーティのホスト型データベース、VM を使って実行されるデータベース サービス、Windows Azure SQL データベースなどを使用でき、おそらく他にもいくつか選択肢があります。今回のバックエンド データベースについては、以下に挙げる複数の理由で、VM 内で実行される SQL Server ではなく、Windows Azure SQL データベースを選択しました。まず、基盤となる製品で位置情報ベースのサービスがサポートされます。次に、Windows Azure SQL データベースは、クライアント システムの SQL Server のベースライン インストールよりもパフォーマンスが優れています。最後に、基盤となるシステムを運用中に管理する必要がありません。

Windows Azure SQL データベースには、SQL Server と同じ point 型や geography 型があるため、2 つの地点間の距離を簡単に計算できます。今回はより簡単にするため、このような距離を計算する 2 つのストアド プロシージャを作成しました。最初の SQL 関数 CalculateDistanceViaLatLon は、緯度と経度を浮動小数点値として受け取ります。この関数は WAMS の insert.js スクリプト内で実行するよう設計しているため、ドライバー ショットの飛距離を計算するのは簡単です。計算結果は、システム内のドライバー ショットの現在の最長飛距離と比較できます。もう 1 つの SQL 関数 CalculateDistance は、2 つの geography 型の位置を受け取り、2 つの位置の距離を計算します (図 3 参照)。データは SQL Server の point 型として Drive テーブルに格納されます。

図 3 2 つの位置間の距離を計算する SQL 関数

 

CREATE FUNCTION [dbo].[CalculateDistanceViaLatLon]
(
  @lat1 float,
  @lon1 float,
  @lat2 float,
  @lon2 float
)
RETURNS float
AS
BEGIN
  declare @g1 sys.geography = sys.geography::Point(@lat1, @lon1, 4326)
  declare @g2 sys.geography = sys.geography::Point(@lat2, @lon2, 4326)
  RETURN @g1.STDistance(@g2)
END
CREATE FUNCTION [dbo].[CalculateDistance]
(
  @param1 [sys].[geography],
  @param2 [sys].[geography]
)
RETURNS INT
AS
BEGIN
  RETURN @param1.STDistance(@param2)
END

図 4 に、ドライバー ショットについてのデータを保持するテーブルを示します。"__" がプレフィックスとして付いた列は Windows Azure 特有の列なので、可能な限りこのような列は使用しないようにします。注目する列は StartingPoint、EndingPoint、Distance、deviceToken、および deviceType です。StartingPoint 列と EndingPoint 列は、ティーグランドとボールの落下点の地理上の位置を保持します。Distance 列は計算列です。この列は SQL 関数の CalculateDistance を使用する浮動小数点型です。deviceToken 列と deviceType 列は、それぞれデバイスを識別するトークンとデバイスの種類 (Windows Phone ベースまたは iPhone) を保持します。このアプリでは、現状、問い合わせを行ったデバイスに、入力したドライバー ショットの値が新たなトップかどうかを返すだけです。deviceToken 列と deviceType 列を使って、トップが入れ替わったことを伝えたり、競技者の最新情報を定期的に伝えるたりすることもできます。

図 4 ドライバー ショットのデータを保持する SQL テーブル

CREATE TABLE [MsdnMagGolfLongDrive].[Drive] (
  [id]            NVARCHAR (255)
     CONSTRAINT [DF_Drive_id] 
     DEFAULT (CONVERT([nvarchar](255),newid(),(0))) 
     NOT NULL,
  [__createdAt]   DATETIMEOFFSET (3) CONSTRAINT
    [DF_Drive___createdAt] DEFAULT (CONVERT([datetimeoffset](3),
    sysutcdatetime(),(0))) NOT NULL,
  [__updatedAt]   DATETIMEOFFSET (3) NULL,
  [__version]     ROWVERSION         NOT NULL,
  [UserID]        BIGINT             NULL,
  [StartingPoint] [sys].[geography]  NULL,
  [EndingPoint]   [sys].[geography]  NULL,
  [DateEntered]   DATETIME           NULL,
  [DateUpdated]   DATETIME           NULL,
  [StartingTime]  DATETIME           NULL,
  [EndingTime]    DATETIME           NULL,
  [Distance]      AS                 ([dbo].[CalculateDistance]
    ([StartingPoint],[EndingPoint])),
  [Provider]      NVARCHAR (20)      NULL,
  [deviceToken]   NVARCHAR (100)     NULL,
  [deviceType] INT NULL,
  PRIMARY KEY NONCLUSTERED ([id] ASC)
);

動的スキーマ

WAMS の優れた特徴の 1 つは、データベース テーブルのスキーマが既定で動的であることです。スキーマはモバイル デバイスからクライアントに送信される情報に基づいて変更されます。開発から運用に移行するときは、この機能を無効にします。最後に気を付けるのは、プログラミング ミスによる実行中のシステムへのなんらかのスキーマ変更です。この問題は、Windows Azure ポータルの WAMS セクションの [構成] に移動して、[動的スキーマ] オプションをオフにすることで、容易に解決できます。

データへのアクセス

信頼性が低く比較的待機時間の長いネットワーク経由でモバイル デバイスからデータにアクセスするのと、ケーブルで接続され比較的待機時間の短いネットワーク経由でデータにアクセスするのは大きな違いがあります。モバイル デバイスでデータを取得するための原則は 2 つあります。まず、データ アクセスは非同期で行います。モバイル ネットワークの信頼性の低さと待機時間の長さを考えると、UI スレッドをロックするのは不適切です。ユーザーには UI を操作できない理由はわかりません。データの取得に時間がかかりすぎると、デバイスの OS はアプリが停止していると判断し強制終了します。次に、転送するデータは比較的小さくします。多くのレコードをモバイル デバイスに送信すると、モバイル プロバイダーのシステムのネットワークが低速かつ待機時間が長いことから、またノート PC やデスクトップの CPU に比べてモバイル デバイスの CPU がデータを処理する消費電力が低く抑えられていることから問題が生じることがあります。WAMS はこのどちらの問題にも対処します。データ アクセスは非同期で、クエリはページング アルゴリズムを用いて自動的に行われます。これを示すために、挿入と選択の 2 つの操作を見ていきます。

プロキシの使用: REST ベースのサービスを呼び出したことがある開発者であれば、プロキシ サービスが組み込まれていないことが問題になることがお分かりでしょう。そのため、REST の操作は若干ミスが生じやすくなります。操作が不可能というわけではなく、SOAP よりも少し難しくなります。開発を容易にするには、プロキシをローカルに作成します。今回の例で使用したプロキシを図 5 に示します。オブジェクト インスタンスのプロパティは、サーバー スクリプトからアクセスできます。

図 5 REST を操作するためのプロキシ

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
namespace Support
{
  public partial class Drive
  {
    public Drive() {
      DeviceToken = String.Empty;
      ChannelUri = String.Empty;
    }
    [JsonProperty(PropertyName="id")]
    public string Id { get; set; }
    [JsonProperty(PropertyName = "UserID")]
    public Int64 UserID { get; set; }
    [JsonProperty(PropertyName = "Provider")]
    public string Provider { get; set; }
    public double StartingLat { get; set; }
    public double StartingLon { get; set; }
    public double EndingLat { get; set; }
    public double EndingLon { get; set; }
    [JsonProperty(PropertyName = "StartingTime")]
    public DateTime StartingTime { get; set; }
    [JsonProperty(PropertyName = "EndingTime")]
    public DateTime EndingTime { get; set; }
    [JsonProperty(PropertyName = "Distance")]
    public double Distance { get; set; }
    [JsonProperty(PropertyName = "deviceType")]
    public int deviceType { get; set; }
    [JsonProperty(PropertyName = "deviceToken")]
    public string DeviceToken { get; set; }
    [JsonProperty(PropertyName = "ChannelUri")]
    public string ChannelUri { get; set; }
  }
}

データのクエリ: Windows Azure でのデータのクエリは実にシンプルです。データは LINQ クエリを使った呼び出しから返されます。以下は、データを返すシンプルなクエリを実行するための呼び出しです。

var drives = _app.client.GetTable<Support.Drive>();
var query = drives.OrderByDescending(
  drive => drive.Distance).Skip(startingPoint).Take(PageSize);
var listedDrives = await query.ToListAsync();

今回の例では、トップから順番に下がっていく、ドライバー ショットの最長飛距離のリストが必要です。Windows Phone ベースのデバイスでも iPhone でも、このリストをグリッドにバインドします。データ バインドの部分は異なりますが、データの取得はまったく同じです。

上記のクエリでは Where メソッドを呼び出していませんが、呼び出すのは簡単です。また、アプリにページ切り替えを簡単に追加する方法を示すために、Skip メソッドと Take メソッドを使用しています。図 6 に、Windows Phone ベースのデバイスと iPhone で実行したスコアボードを示します。

The Scoreboard As Depicted on a Windows Phone-Based Device and an iPhone
図 6 Windows Phone ベースのデバイスと iPhone で描画されたスコア ボード

データの挿入

WAMS でのレコードの挿入は簡単です。データ オブジェクトのインスタンスを作成してから、クライアント オブジェクトの InsertAsync メソッドを呼び出すだけです。Windows Phone ベースのデバイスで挿入を行うコードを 図 7 に示します。Xamarin.iOS で挿入を行うコードも同様で、違うのは deviceType、ChannelUri、および DeviceToken の部分だけです。

図 7 データの挿入 (Windows Phone ベースのデバイス)

async void PostDrive()
{
  Drive d = new Drive();
  d.StartingLat = first.Latitude;
  d.StartingLon = first.Longitude;
  d.EndingLat = second.Latitude;
  d.EndingLon = second.Longitude;
  d.StartingTime = startingTime;
  d.EndingTime = endingTime;
  d.deviceType = (int)Support.AppConstants.DeviceType.WindowsPhone8;
  d.ChannelUri = _app.CurrentChannel.ChannelUri.ToString();
  try
  {
    await _app.client.GetTable<Support.Drive>().InsertAsync(d);
  }
  catch (System.Exception exc)
  {
    Console.WriteLine(exc.Message);
  }
}

プラットフォーム間でコードを共有する

プラットフォームでコードを共有することを考えるのは重要で、C# と Xamarin のクロスプラットフォーム機能を使って複数の方法で実現できます。使用するメカニズムはシナリオによって決まります。今回は UI ロジック以外を共有する必要がありました。そのためには、ポータブル クラス ライブラリ (PCL) とリンク ファイルという 2 つのオプションがあります。

ポータブル クラス ライブラリ: 多くのプラットフォームは .NET Framework を使用します。このようなプラットフォームには、Windows、Windows Phone、Xbox、Windows Azure など、マイクロソフトがサポートしているプラットフォームがあります。.NET Framework が最初にリリースされたとき (またその後数回のリリースまで)、.NET コードは対象とするプラットフォームごとに再コンパイルする必要がありました。PCL がこの問題を解決します。PCL プロジェクトでは、対象プラットフォームで利用できる定義済み API 一式をサポートするライブラリをセットアップします。プラットフォームの選択は、クラス ライブラリのプロジェクト設定で行います。

マイクロソフトは PCL をサポートすると同時に、昨秋 PCL のライセンスを変更して、マイクロソフト以外のプラットフォームをサポートできるようにしています。これにより、Xamarin Inc. は iOS プラットフォーム、Android プラットフォーム、および OS X で、定義済みマイクロソフト PCL のサポートを提供できるようになりました。

リンク ファイル: PCL はクロスプラットフォーム開発の優れたソリューションです。ただし、機能をいずれかのプラットフォームにしか含めない場合は、リンク ファイルがコード共有の代替手段になります。リンク ファイルのセットアップには、基本 .NET クラス ライブラリ、プラットフォーム固有のクラス ライブラリ、およびプラットフォーム アプリ プロジェクトが含まれます。.NET クラス ライブラリは、プラットフォーム間で共有される汎用のコードを備えています。

プラットフォーム固有のライブラリには 2 種類のファイルが含まれています。1 つは、汎用 .NET クラス ライブラリのリンク ファイルで、もう 1 つは共通の API でも実装はプラットフォームごとに異なるコードを含むリンク ファイルです。クラス ライブラリ プロジェクトのファイルを、プラットフォーム固有クラス ライブラリに「リンクとして追加」するというのが考え方です。これを図 8 に示します。

Using Linked Files
図 8 リンク ファイルの使用

その他のオプション: PCL とリンク ファイルは、コードの共有に使用できる複数のオプションの 2 つにすぎません。その他のオプションには、部分クラス、if/def コンパイラ オプション、オブザーバー パターン、Xamarin.Mobile (および同様のライブラリ)、NuGet (または Xamarin コンポーネント ストア) 経由で利用できるその他のライブラリなどがあります。

部分クラスにより、共有のクラス ライブラリとプラットフォーム固有クラス ライブラリの間で、複数のクラス ファイルを共有できます。既定では、.NET クラス ライブラリとプラットフォーム固有ライブラリの間では名前空間が異なります。部分クラスの最も大きな問題は、名前空間を一致させなければならないことです。部分クラスでよくあるミスは、名前空間を一致させていないことです。

Visual Studio では、if/then コンパイラ オプションを使用して、コンパイルするコードを選択できます。同時に、条件付きコンパイル シンボルとしてプラットフォームを定義できます。これらは、プロジェクトのプロパティでセットアップします (図 9 参照)。この例では、Windows Phone 用のコードを条件付きでコンパイルするため、#if ディレクティブが使用されています。

Defining a Platform as a Conditional Compilation Symbol
図 9 プラットフォームを条件付きコンパイル シンボルとして定義する

Xamarin.Mobile は共通 API を含む一連のライブラリです。ライブラリは、Windows Phone、iOS、および Android 向けがあります。Xamarin.Mobile は、現時点で位置情報サービス、連絡先、およびカメラをサポートします。今回のアプリでは、Xamarin.Mobile の位置情報 API を使用しています。

位置を判断するための Geolocator オブジェクトは、プラットフォーム固有の位置情報オブジェクトのラッパーです。以下では C# 5.0 の非同期スタイルの構文を使用しています。位置が判断されると、.ContinueWith を呼び出して処理を行います。

geo = new Geolocator();
...
await geo.GetPositionAsync(timeout: 30000).ContinueWith(t =>
  {
    first = t.Result;
    LandingSpot.IsEnabled = true;
  }, TaskScheduler.FromCurrentSynchronizationContext());

多くのデバイスでは位置情報が近似値として提供されます。そのため、記録されるすべての距離は完全に正確ではありません。

アプリをビルドするとき、開発者の多くは、アプリ上位の論理層から下位レベルへの呼び出しを行うことを考えます。たとえば、ユーザーは位置検出をトリガーするボタンをタッチできます。問題は、アプリ下位の論理レベルから上位層への呼び出しを行う必要があるときに発生します。この場合、上位レベルから下位レベルに参照を渡すのがシンプルなソリューションです。残念ながら、このソリューションでは下位レベルのコードをプラットフォーム間でほぼ共有できなくなります。このような場合はイベントを使用します。つまり、下位レベルでイベントを発生して、上位レベルで処理します。これがオブザーバー パターンの基本です。

多くのサード パーティが、プラットフォーム間で使用できるライブラリを作成しています。このようなライブラリは、NuGet や Xamarin コンポーネント ストアで見つかります。

プッシュ通知

場合によっては、サーバー アプリからモバイル デバイスに通信する必要があります。これは、WAMS または通知ハブ経由で行うことができます。WAMS は、少数のメッセージを送信する際に優れたソリューションです。通知ハブは、「大量の顧客」や「カリフォルニア州の全顧客」など、多数のデバイスにメッセージを一斉送信するために設計されています。今回は WAMS オプションを使用します。

サーバー スクリプト内で、モバイル デバイスへの WAMS プッシュ通知を呼び出すことができます。Windows Azure によってプッシュ通知の複雑な部分の多くが処理されますが、複数のメッセージをさまざまなプラットーフォームに送信する際の違いをなくすことはできません。さいわい、違いはほんのわずかです。

mpns オブジェクトを使用して、Microsoft プッシュ通知サービス (MPNS) 経由でメッセージを送信します。mpns オブジェクトには、sendFlipTile、sendTile、sendToast、sendRaw という 4 つのメンバーがあります。それぞれのメンバーのシグネチャは似ています。最初のパラメーターは通信に使用するチャネルです。2 つ目のパラメーターはデバイスに送信するパラメーターを含む JSON オブジェクトです。3 つ目のパラメーターは、要求が成功または失敗したときに呼び出すコールバックです。mpns オブジェクトを使用する以下のコードを Windows Azure サーバー スクリプト内で使用して、ドライビング コンテストのトップが入れ替わったときにメッセージを送信します。

push.mpns.sendFlipTile(item.ChannelUri, {
  title: "New long drive leader"
}, {
    success: function (pushResponse) {
      console.log("Sent push:", pushResponse);
    }

結果は図 10 に示すタイルの更新です。「ドライビング コンテストのトップが入れ替わった (New long drive leader)」ことを伝えるようにタイルが更新されています。

A Push Message Showing a New Long Drive Leader
図 10 ドライビング コンテストのトップが入れ替わったことを示すプッシュ通知

WAMS スクリプト内で Apple プッシュ通知サービス (APNS) にメッセージを送信するには apns オブジェクトを使用します。apns オブジェクトの考え方は mpns オブジェクトと似ています。最も注目すべきメンバーは send メソッドです。このメソッドは mpns の sendXXX メソッドのシグネチャに似ています。パラメータは、デバイスを一意に識別する deviceToken、JSON ベースのパラメーター オブジェクト、コールバックの 3 つです。

以下は、apns オブジェクトを使用して、「トップが入れ替わった」ことを示すメッセージを iOS デバイスに送信するコードです。

push.apns.send(item.deviceToken, {
  alert: "New Long Drive",
  payload: {
    inAppMessage: "Hey, there is now a new long drive."
  }
});

図 11 に iPhone に送信されるメッセージを処理するために AppDelegate.cs ファイルに追加したコードを示します。今回の例では、UIAlertView をユーザーに表示します。

図 11 iPhone でメッセージを処理

 

public override void RegisteredForRemoteNotifications(
  UIApplication application, NSData deviceToken)
{
  string trimmedDeviceToken = deviceToken.Description;
  if (!string.IsNullOrWhiteSpace(trimmedDeviceToken))
  {
    trimmedDeviceToken = trimmedDeviceToken.Trim('<');
    trimmedDeviceToken = trimmedDeviceToken.Trim('>');
  }
  DeviceToken = trimmedDeviceToken;
}
public override void ReceivedRemoteNotification(
  UIApplication application, NSDictionary userInfo)
{
  System.Diagnostics.Debug.WriteLine(userInfo.ToString());
  NSObject inAppMessage;
  bool success = userInfo.TryGetValue(
    new NSString("inAppMessage"), out inAppMessage);
  if (success)
  {
    var alert = new UIAlertView("Got push notification",
      inAppMessage.ToString(), null, "OK", null);
    alert.Show();
  }
}

 

必要に応じて、gcm オブジェクトを使用して、Google Cloud Messaging (GCM) プラットフォームにメッセージを送信できます。

Windows のプッシュ通知と Apple のプッシュ通知 (および Google の通知) の大きな 1 つの違いは、クライアント モバイル システムがこれらのメッセージを処理する方法です。クライアント システムの完全なリストについては、付属のコード ダウンロードの Xamarin.iOS プロジェクトの AppDelegate.cs ファイルを参照してください。

これですべてです。モバイル アプリの開発とゴルフの試合の幸運を祈ります。

Wallace B. McClure は、ジョージア工科大学 (Georgia Tech) を卒業し、電気工学の学士号と修士号を取得しました。彼はこれまで、大小さまざまな企業でコンサルティングや開発を行ってきました。McClure は、Xamarin.iOS を使った iPhone プログラミング、Xamarin.Android を使った Android プログラミング、アプリ アーキテクチャ、ADO.NET と SQL Server、および AJAX に関する多数の書籍を執筆しています。彼は Microsoft MVP で、ASP、Xamarin MVP、および Xamarin に精通しています。Scalable Development Inc のパートナーでもあります。iOS と Android 向けの彼のトレーニング教材は Learn Now Online から入手できます。彼のブログは morewally.com (英語) から、Twitter は twitter.com/wbm (英語) からご覧いただけます。

この記事のレビューに協力してくれた技術スタッフの Kevin Darty (フリーの契約社員) と Brian Prince (マイクロソフト) に心より感謝いたします。