Windows Phone

Windows Phone と iOS の両方を対象とするアプリを作成する

Andrew Whitechapel

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

iOS から Windows Phone へのアプリの移植に関する資料は多数ありますが、今回は、両方のプラットフォームを対象とする新しいアプリをゼロから作成することを前提に話を始めます。どちらのプラットフォームが優れているといった判断は一切行いません。ただし、実践的な手法をとりながらアプリを作成していくため、途中で明らかになる両プラットフォームの相違点や類似点については説明します。

Windows Phone チームの一員として、Windows Phone プラットフォームに情熱を注いでいます。とは言え、一方のプラットフォームがもう一方のプラットフォームより優れているというつもりはなく、重要な点は、これらのプラットフォームは異なるもので、必要なプログラミング手法も異なっていることです。MonoTouch システムを使用すれば、C# で iOS アプリを作成できますが、こうした環境を使用しているのは少数派です。今回は、iOS には Xcode と Objective-C を、Windows Phone には Visual Studio と C# を使用します。

目標とする UX

目標は、それぞれ対象とするプラットフォームのモデルや原則に従いながら、両方のアプリで同じユーザー エクスペリエンス (UX) を実現することです。これを示すため、Windows Phone 版のアプリでは縦方向にスクロール可能な ListBox を使用してメイン UI を実装し、iOS 版のアプリでは横方向の ScrollViewer を使用して同じ UI を実装することを考えてみます。当然ながら、ここでの違いはソフトウェアだけです。つまり、iOS で縦方向にスクロールするリストを作成し、Windows Phone で横方向にスクロールするリストを作成してもかまいません。しかしこうした好みを持ち込むと、それぞれの設計原則に対する忠実性が失われるため、そうした "不自然な行為" はやめておきましょう。

SeaVan というこのアプリでは、米国のシアトルとカナダのブリティッシュ コロンビア州バンクーバーとの間にある 4 か所の国境検問所と、検問所のそれぞれの車線で国境を越えるまでの待ち時間が表示されます。アプリでは、米国とカナダの両政府の Web サイトから HTTP 経由でデータを取得し、ボタンを使った手動更新またはタイマーを使った自動更新によってデータを最新の情報に更新します。

図 1 にこの 2 つの実装を示します。ここでわかる違いは、Windows Phone 版のアプリがテーマに対応し、現時点のアクセント カラーを使って表示されている点です。これに対してiOS 版のアプリにはテーマがなく、アクセント カラーの考え方もありません。

The Main UI Screen for the SeaVan App on an iPhone and a Windows Phone Device
図 1 iPhone デバイスと Windows Phone デバイスに表示された SeaVan アプリのメイン UI 画面

Windows Phone デバイスには、厳密なページ ベースの線形ナビゲーション モデルがあります。重要な画面 UI はすべてページとして表示され、ユーザーはページ スタックによって前後のページに移動します。iPhone でも同じ線形ナビゲーションを実現できますが、iPhone はこのモデルに制限されていないため、好みに応じてスクリーン モデルを自由に当てはめることができます。iOS 版の SeaVan では、[About] (SeaVan について) ページなどの補助画面は、モーダル ビュー コントローラーです。テクノロジの点から言えば、これらはほぼ Windows Phone のモーダル ポップアップに相当します。

図 2 は、一般化した UI の概略図です。内部 UI 要素は白で、外部 UI 要素 (Windows Phone のランチャー/セレクターや iOS の共有アプリ) はオレンジ色で示しています。設定の UI (黄緑色) は例外として、後半で説明します。

Generalized Application UI
図 2 一般化したアプリ UI

UI でのもう 1 つの違いは、Windows Phone では標準 UI 要素として ApplicationBar が使用される点です。SeaVan では、アプリの補助機能 ([About] (SeaVan について) ページや [Settings] (設定) ページ) を呼び出したり、データを手動で更新したりするためのボタンをこのバーに表示します。iOS には直接 ApplicationBar に相当するものがないため、iOS 版の SeaVan では、シンプルなツールバーを使用して同様の UX を実現します。

反対に、iOS 版のアプリには PageControl があります。PageControl は画面下部にある黒いバーで、場所を示す 4 つの点が表示されます。ユーザーは、コンテンツ自体をスワイプするか PageControl をタップすることで、4 つの国境検問所を横方向にスクロールできます。Windows Phone の SeaVan には、PageControl に相当するものはありません。代わりに、Windows Phone 版の SeaVan のユーザーは、コンテンツを直接スワイプすることで国境検問所をスクロールできます。構成の容易さにより、各ページを連結してすべてを表示できることが、PageControl を使用する効果の 1 つです。Windows Phone のスクロール可能な ListBox では、この操作に関する標準サポートがないため、ユーザーは一部しか表示できず、2 つの国境検問所しか確認することができません。ApplicationBar や PageControl のような UI 要素では、標準動作を使用するだけにとどめ、2 つバージョン間の UX を均一化していません。

アーキテクチャを決定する

どちらのプラットフォームでも、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) アーキテクチャを使用することをお勧めします。1 つ異なる点は、Visual Studio で生成されるコードには、アプリケーション オブジェクトにメインのビューモデルへの参照が含まれることです。Xcode にはこのような参照がないため、ビューモデルはアプリのどこにでも繋げることができます。どちらのプラットフォームでも、ビューモデルをアプリケーション オブジェクトに繋げるのが合理的な方法です。

さらに重要な相違点は、ビューモデルからビューにモデル データが流れるメカニズムです。Windows Phone では、これをデータ バインドによって実現します。データ バインドでは、UI 要素をビューモデル データに関連付ける方法 (およびランタイムで値の実際の伝達を処理する方法) を XAML で指定できます。iOS には、(キー値監視パターンに基づいて) 同様の動作を実行するサードパーティ製のライブラリはあるものの、iOS の標準ライブラリでデータ バインドに相当するものはありません。代わりに、アプリではビューモデルとビューの間でデータ値を手動で伝達する必要があります。図 3 に、SeaVan の一般化したアーキテクチャとコンポーネントを示します。ビューモデルはピンク、ビューは青で示しています。

Generalized SeaVan Architecture
図 3 一般化した SeaVan アーキテクチャ

Objective-C と C#

Objective-C と C# との詳細な比較について、このように短い記事ですべて説明することはもちろんできませんが、主要構造のおおよその対応を図 4 に示します。

図 4 Objective-C の主要構成と対応する C# の構成

Objective-C 概念 C# の対応部分
@interface Foo : Bar {} クラスの宣言、継承を含む class Foo : Bar {}

@implementation Foo

@end

クラスの実装 class Foo : Bar {}
Foo* f = [[Foo alloc] init] クラスのインスタンス作成と初期化 Foo f = new Foo();
-(void) doSomething {} インスタンス メソッドの宣言 void doSomething() {}
+(void) doOther {} クラス メソッドの宣言 static void doOther() {}

[myObject doSomething];

または

myObject.doSomething;

オブジェクトにメッセージを送信する (オブジェクトのメソッドを呼び出す) myObject.doSomething();
[self doSomething] 現在のオブジェクトにメッセージを送信する (現在のオブジェクトのメソッドを呼び出す) this.doSomething();
-(id)init {} 初期化子 (コンストラクター) Foo() {}
-(id)initWithName:(NSString*)n price:(int)p {} パラメーターを受け取る初期化子 (コンストラクター) Foo(String n, int p) {}
@property NSString *name; プロパティの宣言 public String Name { get; set; }
@interface Foo : NSObject <UIAlertViewDelegate> Foo は NSObject をサブクラス化し、UIAlertViewDelegate プロトコル (C# インターフェイスにほぼ相当) を実装する class Foo : IAnother

アプリの主要コンポーネント

SeaVan アプリ作成に着手するには、Xcode で新しい Single View アプリを作成し、Visual Studio で Windows Phone アプリを作成します。どちらのツールでも、一連の初期ファイル (アプリケーション オブジェクトおよびメイン ページやメイン ビューを表すクラスなど) を伴うプロジェクトが作成されます。

iOS の表記法では、クラス名に 2 文字のプレフィックスを使用することになっているため、SeaVan のカスタム クラスにはすべて "SV" というプレフィックスを付けています。iOS アプリでは、アプリ デリゲートを作成する通常の C メイン メソッドから始めます。SeaVan では、これが SVAppDelegate クラスのインスタンスになります。アプリ デリゲートは、Windows Phone の App オブジェクトに相当します。Xcode で、自動参照カウント (ARC) を有効にしてプロジェクトを作成します。これにより、main の全コードを囲むように @autoreleasepool スコープ宣言が追加されます。

int main(int argc, char *argv[])
{
  @autoreleasepool {
    return UIApplicationMain(
    argc, argv, nil, 
    NSStringFromClass([SVAppDelegate class]));
  }
}

これでシステムは、作成するオブジェクトの参照カウントを自動的に数え、カウントが 0 になるとオブジェクトを自動的に解放するようになります。@autoreleasepool により、通常の C/C++ メモリ管理に関するほとんどの問題が適切に処理され、C# に近いコーディング エクスペリエンスがもたらされます。

SVAppDelegate のインターフェイス宣言では、SVAppDelegate に <UIApplicationDelegate> を指定しています。これにより、SVAppDelegate は、アプリ デリゲートの標準メッセージ (application:didFinishLaunchingWithOptions など) に応答するようになります。また、SVContentController プロパティも宣言します。SeaVan では、このプロパティが Windows Phone の MainPage 標準クラスに対応します。最後のプロパティは SVBorderCrossings ポインターです。SVBorderCrossings ポインターはメインのビューモデルで、それぞれが 1 つの国境検問所を示す SVBorderCrossing アイテムのコレクションを保持することになります。

@interface SVAppDelegate : UIResponder <UIApplicationDelegate>{}
@property SVContentController *contentController;
@property SVBorderCrossings *border;
@end

main を開始すると、アプリ デリゲートが初期化され、didFinishLaunchingWithOptions セレクターを含むアプリケーション メッセージがシステムからデリゲートに送信されます。Windows Phone と比べると、このロジックはアプリケーションの Launching イベント ハンドラーまたは Activated イベント ハンドラーに相当します。次に、SVContent という Xcode Interface Builder (XIB) ファイルを読み込み、このファイルを使用してメイン ウィンドウを初期化します。Windows Phone では、XAML ファイルがこの XIB ファイルに相当します。XIB ファイルは、通常、(Visual Studio のグラフィカルな XAML エディターと似ている) Xcode のグラフィカルな XIB エディターによって間接的に編集しますが、実際には XML ファイルです。SVContentController クラスは SVContent.xib ファイルに関連付け、同様に、Windows Phone の MainPage クラスは MainPage.xaml ファイルに関連付けます。

最後に、SVBorderCrossings ビューモデルのインスタンスを作成し、初期化子を呼び出します。iOS では、通常、初期化されていないオブジェクトを使用した場合の潜在的な問題を避けるため、1 つのステートメントで alloc と init を記述します。

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [[NSBundle mainBundle] loadNibNamed:@"SVContent" 
    owner:self options:nil];
  [self.window addSubview:self.contentController.view];
  border = [[SVBorderCrossings alloc] init];
  return YES;
}

Windows Phone では、通常、XIB の読み込み処理に相当する処理がバックグラウンドで実行されます。この部分のコードは、ビルドで XAML ファイルを使用することで生成されます。たとえば、MainPage.g.cs の Obj フォルダーで非表示になっているファイルを表示すると、オブジェクトの XAML を読み込む InitializeComponent メソッドを確認できます。

public partial class MainPage : Microsoft.Phone.Controls.PhoneApplicationPage
{
  public void InitializeComponent()
  {
    System.Windows.Application.LoadComponent(this,
      new System.Uri("/SeaVan;component/MainPage.xaml",
      System.UriKind.Relative));
  }
}

SVContentController クラスはメイン ページとしてスクロール ビューアーをホストし、スクロール ビューアーは 4 つのビュー コントローラーをホストします。最終的に、各ビュー コントローラーには、4 か所にあるシアトル ~ バンクーバー間の国境検問所のうちいずれかの検問所から取得したデータが格納されます。クラスでは <UIScrollViewDelegate> と宣言し、ビュー コントローラーの UIScrollView、UIPageControl、および NSMutableArray という 3 つのプロパティを定義します。scrollView と pageControl は、どちらも IBOutlet プロパティとして宣言します。これにより、XIB エディターで scrollView と pageControl を UI のアーティファクトに結び付けることができます。Windows Phone では、XAML の要素を x:Name で宣言し、クラス フィールドを生成する処理に相当します。iOS でも、クラスで XIB の UI 要素を IBAction プロパティに結び付け、UI イベントをフックすることが可能です。Silverlight で言うと、たとえば、XAML に Click ハンドラーを追加してイベントをフックし、クラス内にイベント ハンドラーのスタブ コードを用意する処理に相当します。興味深いことに、この SVContentController では、UI クラスがサブクラス化されません。代わりに、NSObject 基本クラスがサブクラス化されます。NSObject 基本クラスは <UIScrollViewDelegate> プロトコルを実装する (scrollView メッセージに応答する) ため、SeaVan の UI 要素として機能します。

@interface SVContentController : NSObject <UIScrollViewDelegate>{}
@property IBOutlet UIScrollView *scrollView;
@property IBOutlet UIPageControl *pageControl;
@property NSMutableArray *viewControllers;
@end

SVContentController の実装で最初に呼び出されるメソッドは (NSObject から継承された) awakeFromNib です。ここで、SVViewController オブジェクトの配列を作成して、各ページのビューを scrollView に追加します。

- (void)awakeFromNib
{   
  self.viewControllers = [[NSMutableArray alloc] init];
  for (unsigned i = 0; i < 4; i++)
  {
    SVViewController *controller = [[SVViewController alloc] init];
    [controllers addObject:controller];
    [scrollView addSubview:controller.view];
  }
}

最後に、ユーザーが scrollView をスワイプするかページ コントロールをタップすると、scrollViewDidScroll メッセージを受け取ります。このメソッドでは、前のページまたは次のページが半分以上表示されると、PageControl インジケーターを切り替えます。次に、表示するページと、(ユーザーがスクロールを始めたときにページが点滅したようになるのを避けるため) そのページの前後どちらかのページを読み込みます。ここでの最後の処理は、プライベート メソッドの updateViewFromData を呼び出すことです。このメソッドでは、ビューモデル データを取得して、UI の各フィールドに手動で設定します。

- (void)scrollViewDidScroll:(UIScrollView *)sender
{
  CGFloat pageWidth = scrollView.frame.size.width;
  int page = floor((scrollView.contentOffset.x - 
    pageWidth / 2) / pageWidth) + 1;
  pageControl.currentPage = page;
  [self loadScrollViewWithPage:page - 1];
  [self loadScrollViewWithPage:page];
  [self loadScrollViewWithPage:page + 1];
  [self updateViewFromData];
}

Windows Phone でこれに対応する機能は、XAML での宣言によって MainPage に実装されます。国境を越えるまでの時間を表示するため、ListBox の DataTemplate 内で TextBlock コントロールを使用します。ListBox ではデータの各セットが自動的にビューにスクロールされるため、Windows Phone の SeaVan ではスクロール ジェスチャーを処理するカスタム コードは必要ありません。この処理はデータ バインドによって対処されるため、updateViewFromData メソッドに対応するメソッドはありません。

Web データを取得して解析する

SVAppDelegate クラスは、アプリ デリゲートとして機能するだけでなく、フィールドやプロパティを宣言することで、米国とカナダの Web サイトから取得した越境に関するデータの取得および解析をサポートします。2 つの Web サイトへの HTTP 接続を行うには、2 つの NSURLConnection フィールドを宣言します。また、2 つの NSMutableData フィールド (取得するごとにデータの各ブロックを追加するバッファー) も宣言します。クラスを更新して <NSXMLParserDelegate> プロトコルを実装することで、標準のアプリ デリゲートになるだけでなく、XML パーサーのデリゲートにもなります。XML データを受け取ると、このクラスが最初に呼び出されてデータが解析されます。まったく異なる 2 セットの XML データを処理するので、2 つの子パーサー デリゲートのうちいずれかにすぐに処理を渡します。これを実行するには、SVXMLParserUs と SVXMLParserCa というカスタム フィールドのペアを宣言します。また、このクラスでは、自動更新機能用のタイマーも宣言します。各タイマー イベントでは、refreshData メソッドを呼び出します (図 5 参照)。

図 5 SVAppDelegate のインターフェイス宣言

@interface SVAppDelegate : 
  UIResponder <UIApplicationDelegate, NSXMLParserDelegate>
{
  NSURLConnection *connectionUs;
  NSURLConnection *connectionCa;
  NSMutableData *rawDataUs;
  NSMutableData *rawDataCa;
  SVXMLParserUs *xmlParserUs;
  SVXMLParserCa *xmlParserCa;
  NSTimer *timer;
}
@property SVContentController *contentController;
@property SVBorderCrossings *border;
- (void)refreshData;
@end

refreshData メソッドでは、受け取るデータのセットごとに変更可能なデータ バッファーが割り当てられ、2 つの HTTP 接続が確立されます。iOS のパーサー デリゲート モデルでは同じオブジェクトの要求を 2 つとも開始する必要があり、すべてのデータはこのオブジェクトに返されるため、ここでは NSURLConnection をサブクラス化する SVURLConnectionWithTag カスタム クラスを使用します。そこで、取得する米国のデータとカナダのデータを識別する手段が必要です。そのためには、各接続にタグを付け、NSMutableDictionary で両方の接続をキャッシュします。各接続を初期化する際は、self をデリゲートとして指定します。データのブロックを受け取ったら、connectionDidReceiveData メソッドを呼び出します。この処理は、そのタグが付いているバッファーにデータを追加するために実装します (図 6 参照)。

図 6 HTTP 接続の設定

static NSString *UrlCa = @"http://apps.cbp.gov/bwt/bwt.xml";
static NSString *UrlUs = @"http://wsdot.wa.gov/traffic/rssfeeds/CanadianBorderTrafficData/Default.aspx";
NSMutableDictionary *urlConnectionsByTag;
- (void)refreshData
{
  rawDataUs = [[NSMutableData alloc] init];
  NSURL *url = [NSURL URLWithString:UrlUs];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];   
  connectionUs =
  [[SVURLConnectionWithTag alloc]
    initWithRequest:request
    delegate:self
    startImmediately:YES
    tag:[NSNumber numberWithInt:ConnectionUs]];
    // ... Code omitted: set up the Canadian connection in the same way
}

また、connectionDidFinishLoading を実装することも必要です。(2 つのうちいずれかの接続の) データをすべて受け取ったら、このアプリ デリゲート オブジェクトを最初のパーサーとして設定します。解析メッセージはブロッキング呼び出しです。したがって、解析メッセージが返されたら、コンテンツ コントローラーの updateViewFromData を呼び出して、解析済みデータから UI を更新できるようになります。

- (void)connectionDidFinishLoading:(SVURLConnectionWithTag *)connection
{
  NSXMLParser *parser = 
    [[NSXMLParser alloc] initWithData:
  [urlConnectionsByTag objectForKey:connection.tag]];
  [parser setDelegate:self];
  [parser parse];
  [_contentController updateViewFromData];
}

一般に、XML パーサーには次の 2 種類があります。

  • Simple API for XML (SAX) パーサー: コードをパーサーとして通知し、XML ツリーを順番に処理する
  • ドキュメント オブジェクト モデル (DOM) パーサー: ドキュメント全体を読み取り、別の要素に関して照会できるメモリ内表現を作成する

iOS の既定の NSXMLParser は SAX パーサーです。iOS ではサードパーティ製の DOM パーサーを使用できますが、ここでは、サードパーティ製ライブラリを使用しない標準プラットフォームを比較します。標準パーサーでは、各要素が順に処理され、XML 文書全体における現在のアイテムの位置は把握されません。したがって、SeaVan の親パーサーによって必要なブロックの 1 番外側を処理し、その次に内側にあるブロックを処理するよう子デリゲート パーサーに渡します。

パーサー デリゲート メソッドでは、カナダと米国の XML を区別する簡単なテストを行い、対応する子パーサーのインスタンスを作成して、その子パーサーがそれ以後の現在パーサーになるよう設定します。また、処理できる XML の最終点に達したときに、子パーサーから親パーサーに解析の制御を戻せるように、子の親パーサーを self に設定します (図 7 参照)。

図 7 パーサー デリゲートのメソッド

- (void)connection:(SVURLConnectionWithTag *)connection didReceiveData:(NSData *)data
{
  [[urlConnectionsByTag objectForKey:connection.tag] appendData:data];
}
- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI
  qualifiedName:(NSString *)qName
  attributes:(NSDictionary *)attributeDict
{
  if ([elementName isEqual:@"rss"]) // start of US data
  {
    xmlParserUs = [[SVXMLParserUs alloc] init];
    [xmlParserUs setParentParserDelegate:self];
    [parser setDelegate:xmlParserUs];
  }
  else if ([elementName isEqual:@"border_wait_time"]) // start of Canadian data
  {
    xmlParserCa = [[SVXMLParserCa alloc] init];
    [xmlParserCa setParentParserDelegate:self];
    [parser setDelegate:xmlParserCa];
  }
}

これに対応する Windows Phone コードでは、まず、米国とカナダの Web サイトに対する Web 要求を設定します。最適なパフォーマンスと応答性を引き出す場合、多くは HttpWebRequest が適していますが、ここでは WebClient を使用します。OpenReadCompleted イベントのハンドラーを設定し、非同期に要求を開きます。

public static void RefreshData()
{
  WebClient webClientUsa = new WebClient();
  webClientUsa.OpenReadCompleted += webClientUs_OpenReadCompleted;
  webClientUsa.OpenReadAsync(new Uri(UrlUs));
  // ... Code omitted: set up the Canadian WebClient in the same way
}

各要求の OpenReadCompleted イベント ハンドラーでは、XML を解析するために Stream オブジェクトとして返されたデータを抽出し、ヘルパー オブジェクトに渡します。2 つの Web 要求と 2 つの OpenReadCompleted イベント ハンドラーはそれぞれ独立しているため、要求にタグを付けたり、取得する特定のデータがどの要求に属しているかをを識別するためにテストを実行したりする必要はありません。また、取得するデータのブロックをいちいち処理して、XML 文書全体を作成する必要もありません。代わりに、すべてのデータが取得されるのをくつろぎながら待っていればよいのです。

private static void webClientUs_OpenReadCompleted(object sender, 
  OpenReadCompletedEventArgs e)
{
  using (Stream result = e.Result)
  {
    CrossingXmlParser.ParseXmlUs(result);
  }
}

iOS とは対照的に、Silverlight には XML の解析用に DOM パーサーが既定で組み込まれています (DOM パーサーは XDocument クラスで表されます)。したがって、パーサーの階層ではなく XDocument を使用して、すべての解析処理を直接実行することができます。

internal static void ParseXmlUs(Stream result)
{
  XDocument xdoc = XDocument.Load(result);
  XElement lastUpdateElement = 
    xdoc.Descendants("last_update").First();
  // ... Etc.
}

ビューとサービスをサポートする

Windows Phone では、App オブジェクトは静的でアプリの他のどのコンポーネントでも使用できます。同様に、iOS では、1 つの UIApplication デリゲート型をアプリで使用することができます。作業を簡略化するため、アプリ内のどの場所でも使用できて、アプリ デリゲートを取得する (特定の SVAppDelegate 型に適切にキャストする) マクロを定義しましょう。

#define appDelegate ((SVAppDelegate *) [[UIApplication sharedApplication] delegate])

このようにすると、たとえば、ユーザーが [Refresh] (更新) ボタン (ビュー コントローラーに属するボタン) をタップしたときに、アプリ デリゲートの refreshData メソッドを呼び出すことが可能です。

- (IBAction)refreshClicked:(id)sender
{
  [appDelegate refreshData];
}

ユーザーが [About] (SeaVan について) ボタンをタップすると、[About] (SeaVan について) ページが表示されるようにしましょう (図 8 参照)。iOS では、SVAboutViewController のインスタンスを、ツールバーに追加する 3 つのボタンと一緒に作成します。SVAboutViewController には、ユーザー ガイド用にスクロール可能なテキスト要素が組み込まれ、XIB が関連付けられています。

The SeaVan About Screen in iOS and Windows Phone
図 8 iOS と Windows Phone に表示された SeaVan の [About] (SeaVan について) ページ

このビュー コントローラーを表示するためインスタンスを作成して、現在オブジェクト (self) に presentModalViewController メッセージを送信します。

- (IBAction)aboutClicked:(id)sender
{
  SVAboutViewController *aboutView =
    [[SVAboutViewController alloc] init];
  [self presentModalViewController:aboutView animated:YES];
}

SVAboutViewController クラスで、このビュー コントローラーを破棄し、制御をビュー コントローラーの呼び出しに戻す [Cancel] (キャンセル) ボタンを実装します。

- (IBAction) cancelClicked:(id)sender
{
  [self dismissModalViewControllerAnimated:YES];
}

組み込みアプリ (電子メール、携帯電話、SMS など) の機能を呼び出すための標準の方法が、どちらのプラットフォームにも用意されています。重要な違いは、組み込みの機能が終了した後に、アプリに制御が戻るかどうかです。Windows Phone では、常にアプリに制御が戻りますが、iOS では、アプリに制御が戻る機能と戻らない機能があります。

SVAboutViewController で、ユーザーが [Support] (サポート) ボタンをタップしたときに、開発チームに送信する電子メールを作成するようにします。MFMailComposeViewController (これもモーダルなビューとして示されます) は、この用途において優れた効果を発揮します。この標準ビュー コントローラーは、[Cancel] (キャンセル) ボタンも実装しています。このボタンは、このボタン自体を破棄してまったく同じ処理を実行し、呼び出しているビューに制御を戻します。

- (IBAction)supportClicked:(id)sender
{
  if ([MFMailComposeViewController canSendMail])
  {
    MFMailComposeViewController *mailComposer =
      [[MFMailComposeViewController alloc] init];
    [mailComposer setToRecipients:
      [NSArray arrayWithObject:@"tensecondapps@live.com"]];
    [mailComposer setSubject:@"Feedback for SeaVan"];
    [self presentModalViewController:mailComposer animated:YES];
}

iOS で地図の方角を認識する標準の方法は、Google マップを呼び出すことです。この手法の短所は、ユーザーが Safari 共有アプリ (組み込みアプリ) の使用を停止する必要があるうえ、プログラムでアプリに制御を戻す手段がないことです。ユーザーがアプリから離れる場所はできるだけ少なくしたいので、標準の MKMapView コントロールをホストするカスタムの SVMapViewController を使用して、方角ではなく対象の国境検問所の地図を表示します。

- (IBAction)mapClicked:(id)sender
{   
  SVBorderCrossing *crossing =
    [appDelegate.border.crossings
    objectAtIndex:parentController.pageControl.currentPage];
  CLLocationCoordinate2D target = crossing.coordinatesUs;
  SVMapViewController *mapView =
    [[SVMapViewController alloc]
    initWithCoordinate:target title:crossing.portName];
  [self presentModalViewController:mapView animated:YES];
}

ユーザーがレビューを入力できるようにするため、iTunes App Store にアプリへのリンクを追加します (次のコード内の 9 桁の ID は、アプリの App Store ID です)。次に、このリンクを Safari ブラウザー (共有アプリ) に渡します。次に行うことは、アプリを離れることだけです。

- (IBAction)appStoreClicked:(id)sender
{
  NSString *appStoreURL =
    @"http://itunes.apple.com/us/app/id123456789?mt=8";
  [[UIApplication sharedApplication]
    openURL:[NSURL URLWithString:appStoreURL]];
}

Windows Phone で [About] (SeaVan について) ボタンに相当するものは、ApplicationBar のボタンです。ユーザーがこのボタンをタップすると、NavigationService を呼び出して AboutPage に移動します。

private void appBarAbout_Click(object sender, EventArgs e)
{
  NavigationService.Navigate(new Uri("/AboutPage.xaml", 
    UriKind.Relative));
}

iOS 版と同様に、AboutPage では、スクロール可能なテキストによる簡単なユーザー ガイドを表示します。ユーザーはハードウェアの戻るボタンを押してこのページに戻ることができるため、[Cancel] (キャンセル) ボタンは作成しません。[Support] (サポート) ボタンおよび [App Store] ボタンに代わり、HyperlinkButton コントロールを使用します。サポートの電子メールについては、mailto: プロトコルを指定する NavigateUri を使用することで、宣言によって動作を実装します。EmailComposeTask を呼び出すには、これで十分です。

<HyperlinkButton 
  Content="tensecondapps@live.com" 
  Margin="-12,0,0,0" HorizontalAlignment="Left"
  NavigateUri="mailto:tensecondapps@live.com" 
  TargetName="_blank" />

コード内の Click ハンドラーによって Review リンクを設定し、MarketplaceReviewTask ランチャーを呼び出します。

private void ratingLink_Click(object sender, 
  RoutedEventArgs e)
{
  MarketplaceReviewTask reviewTask = 
    new MarketplaceReviewTask();
  reviewTask.Show();
}

MainPage に戻り、Map/Directions (地図/方角) 機能のボタンを個別に作成するのではなく、ListBox に SelectionChanged イベントを実装します。これで、ユーザーはコンテンツをタップしてこの機能を呼び出せるようになります。この手法は、Windows ストア アプリに適合しているため、ユーザーはクローム要素から間接的に操作するのではなく、コンテンツによってアプリを直接操作します。このハンドラーでは、BingMapsDirectionsTask ランチャーを起動します。

private void CrossingsList_SelectionChanged(
  object sender, SelectionChangedEventArgs e)
{
  BorderCrossing crossing = (BorderCrossing)CrossingsList.SelectedItem;
  BingMapsDirectionsTask directions = new BingMapsDirectionsTask();
  directions.End =
    new LabeledMapLocation(crossing.PortName, crossing.Coordinates);
  directions.Show();
}

アプリの設定

iOS プラットフォームでは、アプリの環境設定は組み込みの設定アプリによって一元管理されます。設定アプリには、ユーザーが組み込みのアプリとサードパーティ製のアプリ両方の設定を編集できる UI が用意されています。図 9 に、iOS における主要な設定 UI と SeaVan の具体的な [設定] ページ、および Windows Phone の [settings] (設定) ページを示します。SeaVan に関する設定は、自動更新機能の切り替えを行う 1 つだけです。

Standard Settings and SeaVan-Specific Settings on iOS and the Windows Phone Settings Page
図 9 iOS の [設定] ページと Windows Phone の [Settings] (設定) ページでの標準設定と SeaVan 固有の設定

設定をアプリ内に組み込むには、Xcode を使用して Settings バンドルと呼ばれる特殊なリソースを作成します。次に、Xcode の設定エディターを使用して設定値を指定します。これにはコードは必要ありません。

Application メソッドでは、設定を同期して、ストアから現在値を取得します (図 10 参照)。自動更新の設定値が True の場合は、タイマーを開始します。API では、アプリ内での取得と設定の両方がサポートされているため、設定アプリ内でのアプリのページに加えて、アプリ内で [Settings] (設定) ページを提供することが可能です。

図 10 Application メソッド

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSUserDefaults *defaults =
    [NSUserDefaults standardUserDefaults];
  [defaults synchronize];
  boolean_t isAutoRefreshOn =
    [defaults boolForKey:@"autorefresh"];
  if (isAutoRefreshOn)
  {
    [timer invalidate];
    timer =
      [NSTimer scheduledTimerWithTimeInterval:kRefreshIntervalInSeconds
        target:self
        selector:@selector(onTimer)
        userInfo:nil
        repeats:YES];
  }
  // ... Code omitted for brevity
  return YES;
}

Windows Phone では、グローバルな設定アプリにアプリ設定を追加することはできません。代わりに、アプリ内で独自の設定 UI を使用します。SeaVan では、AboutPage と同様、SettingsPage は単なる別のページにすぎません。ApplicationBar に、このページに移動するボタンを表示します。

private void appBarSettings_Click(object sender, 
  EventArgs e)
{
  NavigationService.Navigate(new Uri("/SettingsPage.xaml", 
    UriKind.Relative));
}

SettingsPage.xaml で、自動更新機能の ToggleSwitch を定義します。

<StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
  <toolkit:ToggleSwitch
    x:Name="autoRefreshSetting" Header="auto-refresh"
    IsChecked="{Binding Source={StaticResource appSettings},
    Path=AutoRefreshSetting, Mode=TwoWay}"/>
</StackPanel>

アプリ内では設定ビヘイビアを指定するしかありません。ただし、これを有効に利用し、設定ビヘイビア用に AppSettings ビューモデルを実装して、その他のデータ モデルと同じようにデータ バインドによってビューにフックすることが可能です。MainPage クラスでは、設定値に基づいてタイマーを開始します。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  if (App.AppSettings.AutoRefreshSetting)
  {
    timer.Tick += timer_Tick;
    timer.Start();
  }
}

バージョン情報およびサンプル アプリ

プラットフォームのバージョン

  • Windows Phone SDK 7.1 および Silverlight for Windows Phone Toolkit
  • iOS 5 および Xcode 4

SeaVan は、Windows Phone Marketplace と iTunes App Store の両方に公開予定です。

難易度はそれほど高くない

iOS と Windows Phone の両方を対象とする 1 つのアプリを作成することは、それほど難しくありません。相違点よりも類似点の方が多いためです。1 つの App オブジェクトによって MVVM を使用しても、1 つ以上の Page/View オブジェクトによって MVVM を使用しても、UI クラスはグラフィカル エディターで編集する XML (XAML または XIB) に関連付けられます。iOS ではオブジェクトにメッセージを送信し、Windows Phone ではオブジェクトのメソッドを呼び出します。しかし、ここでの相違点はほとんど観念的なもので、"[メッセージ]" という表記が気に入らなければドット表記を使用することも可能です。イベント/デリゲート メカニズム、インスタンス メソッドと静的メソッド、プライベート メンバーとパブリック メンバー、および get アクセサーと set アクセサーのあるプロパティは、両方のプラットフォームに用意されています。両方のプラットフォームで、アプリに組み込まれた機能を呼び出してユーザー設定をサポートすることができます。もちろん 2 つのコードベースを管理する必要はありますが、アプリのアーキテクチャ、主要なコンポーネント設計、および UX は、プラットフォーム間で一貫性を保つことが可能です。試してみてください。きっとうれしい驚きがあります。

Andrew Whitechapel は、20 年以上にわたって開発に携わっており、現在は Windows Phone チームのプログラム マネージャーとしてアプリケーション プラットフォームの中核部分を担当しています。彼の新しい著書は、『Windows Phone 7 Development Internals』(Microsoft Press、2012 年) です。

この記事のレビューに協力してくれた技術スタッフの Chung Webster と Jeff Wilcox に心より感謝いたします。