サンプルプログラムで理解するXamarin.Formsの特徴:特集:Xamarin.Formsを知る(3/3 ページ)
現在、大きな注目を集めているXamarin.Formsとは何か。サンプルコードを解剖しながら、その特徴を調べてみよう。
共有コード
以下では3つのアプリプロジェクトで共有されるコードの中でポイントとなる部分を幾つか紹介する。
XAML
まずは3つのアプリプロジェクトで共有されるUIから見てみよう。
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Phoneword.MainPage">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness" iOS="20, 40, 20, 20" Android="20, 20, 20, 20" WinPhone="20, 20, 20, 20" />
</ContentPage.Padding>
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" Orientation="Vertical" Spacing="15">
<Label Text="Enter a Phoneword:" />
<Entry x:Name="phoneNumberText" Text="1-855-XAMARIN" />
<Button x:Name="translateButon" Text="Translate" Clicked="OnTranslate" />
<Button x:Name="callButton" Text="Call" IsEnabled="false" Clicked="OnCall" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
見慣れたXAMLコードだ。ただし、スキーマとして「http://xamarin.com/schemas/2014/forms」が含まれていることに注意。これによりXamarin.Formsで定義されている要素をXAMLに記述できるように名前空間が導入されている(強調表示部分。もう1つの強調表示部分については後述する)。
細かな要素から順に見ていくと、このUIには1つのラベル(<Label>要素)、1つのデータ入力ボックス(<Entry>要素)、2つのボタン(<Button>要素)が含まれている。これらは<StackLayout>要素に含まれていて、そのOrientation属性が「Vertical」になっているので、縦に4つのUI要素がレイアウトされることになる。そして、<StackLayout>要素が<ContentPage>要素のContent属性に含まれていて、このページの内容(コンテンツ)が<StackLayout>要素で表されていることが分かる。この辺の構造は通常のXAMLと同様だ。
今述べた要素は全てXamarin.Formsで定義されている「コントロール」だ。冒頭で述べたようにXamarin.FormsはiOS、Android、Windows(ここではUWP)のUIを抽象化したレイヤーであり、Xamarin.Formsで定義されているコントロールは、アプリの実行時にはそれぞれのOSにネイティブなコントロールを使用して描画が行われる。
Xamarin.Formsのコントロールは実際にはOSにネイティブなコントロールを使って描画される
左はiPhone Emulatorでの表示。中はVisual Studio Emulator for Androidでの表示。右はWindows 10 Mobileエミュレーターでの表示。
[Translate]ボタンと[Call 〜]ボタンに注目すると、iOSアプリではボタンの境界線がないことに、Androidアプリでは文字が全て大文字化されていることが分かる。テキストボックスに枠線があるのはUWPアプリだけだ。XAMLは共通しても、各OSにネイティブなコントロールで(今回はデフォルトの設定を使って)描画が行われているために、このような差異が出てくる。
これらのコントロールは大きく以下の4種類に分類されている(前掲の図も参照のこと)。
- ページ: 画面全体を占拠するUI要素。上のXAMLでは<ContentPage>要素が該当
- レイアウト: 他のレイアウトやビューのコンテナとなり、それらをどう表示するのかのレイアウトを指定する。上のXAMLでは<StackLayout>要素が該当
- ビュー: ボタンやラベル、テキストボックスなどの一般的なコントロール。上のXAMLでは<Button>要素、<Label>要素、<Entry>要素が該当
- セル: リストビューや表中に要素を表示するために使われる。例えば、EntryCellコントロールは「ラベル+1行入力テキストエントリ」としてXamarin.Formsでは扱われ、iOSでは「UITableViewCellとUITextField」、Androidでは「TextViewとEditTextを含んだLinearLayout」、UWPでは「TextBoxを含んだデータテンプレート」を使って描画される
このように、3つのアプリプロジェクトではXAMLを共有するが、実際のアプリの画面表示ではOSにネイティブなUI要素が使われる。またXAMLは共有されるが、その中でOS依存の設定を行うこともできる。これを行っているのが以下のコードだ(コードが見やすくなるように改行を挿入している)。
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="20, 40, 20, 20"
Android="20, 20, 20, 20"
WinPhone="20, 20, 20, 20" />
</ContentPage.Padding>
これはページに表示するコンテンツとページ境界との間にどれだけの空きを含めるかを指定するXAMLだが、<OnPlatform>属性を用いてiOSだけは空きの量を変更している(「40」に注目)。これはOSごとに異なる挙動を記述する1つの方法だ。C#コードでも「Device.OnPlatform」メソッドを使うことで、OSによって処理を変更できる。
IDialerインタフェース
共有されるコードでもう1つ重要な部分を以下に示す。これは、上で示した実行画面で[Call]ボタンがクリックされたときに実行されるコードだ。
public partial class MainPage : ContentPage
{
string translatedNumber;
public MainPage()
{
InitializeComponent();
}
void OnTranslate(object sender, EventArgs e)
{
…… [Translate]ボタンがクリックされたら、Phone wordsを数字に変換する ……
}
async void OnCall(object sender, EventArgs e)
{
if (await this.DisplayAlert(
"Dial a Number",
"Would you like to call " + translatedNumber + "?",
"Yes",
"No"))
{
var dialer = DependencyService.Get<IDialer>();
if (dialer != null)
dialer.Dial(translatedNumber);
}
}
}
PCLプロジェクトは、3つのアプリプロジェクトから参照されるだけで、PCLからアプリを参照はしない。そのため、アプリプロジェクトで実装するOS依存の機能はこのコードのようにDependencyServiceクラスを利用して、依存性注入の形で呼び出す。先も見た通り、IDialerインタフェースはdialメソッドのみを持つインタフェースだ。そして、これを実装するのが次に紹介する3つのPhoneDialer.csファイルだ。
共有しないコード
3つのアプリプロジェクトではIDialerインタフェースを実装するPhoneDialerクラスを定義している。スマートフォンでは電話をかけるためのコードはOSごとに異なる。そのため、各アプリプロジェクトで個別に実装する必要がある。例えば、Androidではインテントを使用して、以下のようなコードを記述する。
using Android.Content;
using Android.Telephony;
using Phoneword.Droid;
using System.Linq;
using Xamarin.Forms;
using Uri = Android.Net.Uri;
[assembly: Dependency(typeof(PhoneDialer))]
namespace Phoneword.Droid
{
public class PhoneDialer : IDialer
{
public bool Dial(string number)
{
var context = Forms.Context;
if (context == null)
return false;
var intent = new Intent(Intent.ActionCall);
intent.SetData(Uri.Parse("tel:" + number));
if (IsIntentAvailable(context, intent))
{
context.StartActivity(intent);
return true;
}
return false;
}
public static bool IsIntentAvailable(Context context, Intent intent)
{
…… 中略 ……
}
}
}
このようにOSが提供する機能を使ってインタフェースを実装すると共に、assembly属性でDependencyServiceクラスに対して登録を行う(強調表示部分)。このようにすることで、上で見た「DependencyService.Get<IDialer>()」というコードでこのPhoneDialerクラスのインスタンスが取得できる。後は、そのdialメソッドを呼び出すだけだ。他のOSでのIDialerインタフェース実装については省略するが、実装がどんなコードになるかはともかく基本的な考えはこれと同様だ。
まとめ
本稿では、Xamarinが提供するサンプルプログラム「Phoneword」のコードを分解しながら、Xamarin.Formsアプリの特徴を幾つか見てきた。ポイントを以下にまとめておく。
- PCLプロジェクトにはアプリプロジェクトで共有できるロジック、UI、OSごとに異なる実装を抽象化したインタフェースなどをまとめる
- Xamarin.Formsは独自のコントロールを提供し、それらはアプリ実行時にそれをホストするOSにネイティブなコントロールを用いて描画される
- XAMLの内部でもOSごとに設定を切り替えられる(<OnPlatform>要素)
- Device.OnPlatformメソッドを使うと、OSごとに処理を切り替えられる
- ある機能をOSごとに個別に実装する場合、PCL側ではインタフェースを定義すると共にDependencyServiceクラスを用いて、そのインタフェース実装のインスタンスを取得する
- アプリプロジェクト側では、OSが提供する機能を用いて、その機能を実装し、assembly属性を用いてDependencyServiceに登録を行うことで、PCL側がその実装を取得できるようにする
本稿では紹介しきれなかったが、自分でOSネイティブなコントロールを利用したり、Xamarin.Formsのコントロールのカスタマイズを行ったりする機構としてはカスタムレンダラーも用意されている。なお、次回以降では、Xamarin.Formsプログラミングのポイントをより詳細に紹介していく予定だ。
Copyright© Digital Advantage Corp. All Rights Reserved.