ちょうど Xamarin.iOS のアップデートがあって、Xamarin.iOS10 と monotouch が混乱している状態で苦労したのですが、一応動くところまでできたので記事を書いておきます。
目標
Xamarin.iOS(storyboard) と MvvmCross と Xamarin.Froms を混在させたアプリを作ります。単純に混在させるというよりも、アプリの歴史的な経緯から、
- Xamarin.iOS で storyboard で作っているアプリがあって、
- 去年あたりに MvvmCross を使って、MVVM 対応したアプリになって、
- 今年ぐらいから、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