Xamarin.iOS(storybaord)+MvvmCross+Xamarin.Forms を混在させたアプリを作る

ちょうど Xamarin.iOS のアップデートがあって、Xamarin.iOS10 と monotouch が混乱している状態で苦労したのですが、一応動くところまでできたので記事を書いておきます。

目標

Xamarin.iOS(storyboard) と MvvmCross と Xamarin.Froms を混在させたアプリを作ります。単純に混在させるというよりも、アプリの歴史的な経緯から、

  1. Xamarin.iOS で storyboard で作っているアプリがあって、
  2. 去年あたりに MvvmCross を使って、MVVM 対応したアプリになって、
  3. 今年ぐらいから、Xamarin.Forms に対応したいけどどうしようか?

のようなストーリーを考えています。まあ、MVVM 対応するのは MvvmCross でもよいし、MvvmLight でも Prism でも良いわけですが、そこに Xamarin.Froms の XAML をどうねじ込むか、ってのが問題になりますよね。最初から、Xamarin.Forms で作り直してしまう方法もあるけど労力的に大変だし、そもそも Xamarin.Forms のコントロールは非力なので、そのまま移植できないパターンも多い。DepnencyService とか作ればいいけど、面倒だったら、もともと storyboard と Xamarin.iOS の組み合わせで数ページだけ作るのが簡単ではないか?というパターンです。

アプリの想定

こんな風に、Master-Detail で作っていたアプリに対して、MvvvmCross や Xamarin.Forms のページを追加していきます。

これによって、既存のページはそのままにして、新しいページを MvvmCross や Xamarin.Forms で作れればよいかなと。

プロジェクト構成

storyboard を含むのが、MvxXForms.UI.Touch プロジェクトで Detail ページ用にそれぞれのクラスを設定しています。Mater-Detail の Master がリストの場合は詳細ページは同じページを使うことが多いのですが、Mater が固定ページ(メニューページの代わり)に使っている場合には、項目をクリックしたときにそれぞれの詳細ページが表示されるので、こういう構成にしてあります。Master ページにボタンを並べて画面遷移する場合も似た感じになります。

Xamarin.iOS+storybardのみ場合

最初は、MvxXForms.UI.Touch プロジェクト のようなプロジェクトがあって、まだ MVVM 化されていない状態を考えます。

storyboard は、

– Navigation Controller
– Master ページ
– オレンジ色の Detail ページ

だけの状態になります。Master から Detail へ遷移させる場合は、

Creating an Unwind Segue | Xamarin
http://developer.xamarin.com/recipes/ios/general/storyboard/unwind_segue/

な感じで Ctrl キーを押しながらマウスのドラッグで線が引けます。
サンプルコードでは、RowSelected 内でデータを引き渡すために小細工をしていますが、storyboard segue を使えば画面遷移だけならばノンコーディングでいけます。

MvvmCross のページを追加する

MVVM 化するために、MvxXForms.Core プロジェクトを追加します。ViewModel 自体は、先の MvxXForms.UI.Touch に追加してしまってもよいのですが、先行き Android と共有させることを考えて PCL プロジェクトで作っておきます。

int と string を持つ TipViewModel クラスを定義して、

public class TipViewModel : MvxViewModel
{
	public TipViewModel()
	{
	}

	int _pageNum;
	public int PageNum
	{
		get { return _pageNum; }
		set { _pageNum = value; RaisePropertyChanged(() => PageNum); }
	}
	string _Name;
	public string Name
	{
		get { return _Name; }
		set { _Name = value; RaisePropertyChanged(() => Name); }
	}
}

中身が空っぽな App クラスを作っておきます。App クラスを MvxXForms.UI.Touch プロジェクトから参照しなければよいのですが、まあ、これは初期化のためのお約束コードということで。

public class App : MvxApplication
{
	public App()
	{
	}
}

MvxXForms.UI.Touch プロジェクトに戻って、UIApplicationDelegate を MvxApplicationDelegate に変更。
FinishedLaunching メソッドをオーバーライドして、MvvmCross の初期化を行います。

public partial class AppDelegate : MvxApplicationDelegate
{
	// class-level declarations

	public override UIWindow Window
	{
		get;
		set;
	}

	public override bool FinishedLaunching(UIApplication app, NSDictionary options)
	{
		var presenter = new MvxTouchViewPresenter(this, Window);
		var setup = new Setup(this, presenter);
		setup.Initialize();
		return true;
	}
}

あとは、Detail ページに対応する ViewController を MvxViewController から継承させて、set.Bind 等でバインドを行えば ok です。

public partial class Detail2ViewController : MvxViewController
{
	public Detail2ViewController(IntPtr handle)
		: base(handle)
	{
	}

	public new TipViewModel ViewModel
	{
		get { return (TipViewModel)base.ViewModel; }
		set { base.ViewModel = value; }
	}

	public override void DidReceiveMemoryWarning()
	{
		// Releases the view if it doesn't have a superview.
		base.DidReceiveMemoryWarning();

		// Release any cached data, images, etc that aren't in use.
	}

	public override void ViewDidLoad()
	{
		this.Request = new MvxViewModelRequest(typeof(TipViewModel), null, null, new MvxRequestedBy());
		base.ViewDidLoad();
		// Perform any additional setup after loading the view, typically from a nib.
		var set = this.CreateBindingSet<Detail2ViewController, TipViewModel>();
		set.Bind(labelPageNum).To(vm => vm.PageNum);
		set.Bind(labelName).To(vm => vm.Name);
		set.Apply();

		// マスターからのデータ引き渡し
		this.ViewModel = MasterViewController._datavm;
	}
}

storyboard の Detail ページと Detail2ViewController の結び付けは、プロパティウィンドウで Class を変更します。このあたりは Xcode と同じですね。

MasterViewController._datavm なところは、storyboard segue を使うと、内部的に一気に ViewController が作られてまうので、ViewModel プロパティを設定するタイミングがないため、こうやっています。Master ページで Cell をクリックしたときに、下記な方法でグローバル変数で渡します。ちょっとダサいんですが、仕方がありません。

public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
	var data = new MyData();
	switch ( indexPath.Row )
	{
...
		case 1:
			/// MvvmCross を使って vm 経由でデータを渡す
			/// 本来は data 経由のほうがいいけど、これはサンプルで
			/// あらかじめ storyboard segue でつなげておく
			_datavm = new TipViewModel()
			{
				PageNum = 2,
				Name = "use MvvmCross"
			};
			break;
...
}

storyboard segue を使わずに画面遷移をする

じゃあ、明示的に遷移先の ViewController を作って ViewModel を設定する方法でもよいだろう、というのが次の方法です。
目的の ViewController の「storyboard id」に、あらかじめ「Detail3ViewController」という名前を付けておいて(これはクラス名と異なっていても構いません)、Storyboard.InstantiateViewController メソッドで作成します。これを、NavigationController.ShowViewController メソッドで表示すれば ok です。

/// <summary>
/// 行をクリックしたとき
/// </summary>
/// <param name="tableView"></param>
/// <param name="indexPath"></param>
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
	var data = new MyData();
	switch ( indexPath.Row )
	{
...
		case 2:
            /// storyboard のページを直接開く
			/// Storyboard ID を ViewController に設定しておく
			/// storyboard segue を使わないパターン
			var vc = (Detail3ViewController)Storyboard.InstantiateViewController("Detail3ViewController");
			this.NavigationController.ShowViewController(vc, this);
			_datavm2 = new TipViewModel2()
			{
				PageNum = 3,
				Name = "Mvx + direct storyboard"
			};
			break;

遷移先の ViewController を MvxViewController を継承するようにして、ViewModel に対応させてもそのまま使えます。
ちょっと注意しなければいけないのは、MvvmCross では ViewModel と ViewController が 1対1 じゃないと駄目なようです。実行時に TipViewModel が二つ以上の ViewController に設定されている、とエラーがでます。なので、仕方がないので TipViewModel2 という同じ中身のクラス(継承しているだけ)を使っているのですが。同じ ViewModel を複数の View に対応しても良いと思うのですが、ちょっとこの動きはよくわかりません。

Xamrin.Forms のページを呼び出す

Xamarin.Forms の XAML ページを Master ページから呼び出せるようにします。
MvxXForms.Form プロジェクトを別に作っていますが、たぶん、MvxXForms.UI.Touch に含ませてしまっても大丈夫だと思います。

DetailXFPage.xaml の中身を手書きします(Xamarin Studio を使うと、少しだけコード補完が効いて楽です)。

<?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="MvxXForms.Form.DetailXFPage"
					   Title="xamarin.forms page"
					   BackgroundColor="Yellow"
					   >
  <StackLayout Padding="10,80,10,10">
    <Label x:Name="label1" Text="Xamarin.Forms page"  />
    <Label Text="PageNum" />
    <Label Text="{Binding PageNum, StringFormat='{0}'}" BackgroundColor="Lime" />
    <Label Text="Name" />
    <Label Text="{Binding Name}"  BackgroundColor="Lime"/>
  </StackLayout>
</ContentPage>

Binding が XAML の中に記述できます。

Xamarin.Forms のプロジェクトでも App クラスがあるのですが、これは GetMainPage メソッドのように PCL プロジェクト内で作成した Page オブジェクトを返すための static メソッドです。なので、同じように、DetailXFPage を new して返すだけのメソッドを作っておきます。

public class App
{
	public static Page GetDetailPage()
	{
		return new DetailXFPage();
	}
}

Xamarin.Forms のページを呼び出すときは、先の storyboard segue を使わない方法と同じように、NavigationController.ShowViewController メソッドを使います。

public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
	var data = new MyData();
	switch ( indexPath.Row )
	{
...
		case 3:
			// Xamarin.Forms ページを開く
			var page = MvxXForms.Form.App.GetDetailPage();
			var vc2 = page.CreateViewController();
			var vm = new TipViewModel()
			{
				PageNum = 4,
				Name = "xamarin froms page"
			};
			page.BindingContext = vm;
			this.NavigationController.ShowViewController(vc2, this);
			break;

ContentPage の BindingContext プロパティに ViewModel のデータを設定すればバインドが完了します。

Xamarin.Forms の初期化のために、AppDelegate クラスに Forms.Init() を追加しておきます。
これで、Xamarin.Forms と MvvmCross が混在できます。

public partial class AppDelegate : MvxApplicationDelegate
{
	// class-level declarations

	public override UIWindow Window
	{
		get;
		set;
	}

	public override bool FinishedLaunching(UIApplication app, NSDictionary options)
	{
		Forms.Init();
		var presenter = new MvxTouchViewPresenter(this, Window);
		var setup = new Setup(this, presenter);
		setup.Initialize();
		return true;
	}
}

実行してみる

こんな風に、Master のページから各種のページに遷移ができます。

サンプルコード

MxSingleApp の中の MixMvxForms
https://github.com/moonmile/MxSingleApp

参考先

Xamarin.iOS ナビゲーションコントローラ – SIN@SAPPOROWORKSの覚書
http://furuya02.hatenablog.com/entry/2014/07/03/035352
Xamarin.iOSでStoryboardとXamarin.Formsを併用するには? – Build Insider
http://www.buildinsider.net/mobile/xamarintips/0006

カテゴリー: C#, Xamarin, iOS パーマリンク