nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Visual Studio and Development Technologies。なお掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Xamarin.Forms Navigation Overview

久しぶりにJXUGでしゃべらせていただきました。

完全に物理バックボタンを忘れていたので、そのうちどこかで追記なり補足します。

jxug.connpass.com

発表に利用した資料はこちらに公開しています。

docs.com

ただ、スライドだけで理解いただけるものを作るスキルがないので、ここに話した内容も要約して載せておこうと思います。
良かったらご覧ください。

f:id:nuitsjp:20170526175454p:plain

発表の概要は以下のとおりです。

f:id:nuitsjp:20170526175546p:plain

発表内容は主に2点です。
ひとつはXamarin.Formsの画面遷移の基本についておさらいを。
そしてもう一つは、アプリケーションを開発する際に画面遷移サービスを構築するために必要な機構と、その実現のアイディアについてになります。

f:id:nuitsjp:20170526175707p:plain

それでは基本の基本から説明していきましょう。 皆さんもご存知の通り、Formsは多数のPageを、提供しています。

f:id:nuitsjp:20170526175809p:plain

Formsでは、これらのPageをPush>Push>Pushとすることで、画面遷移を実現します。 この時、Pushした画面はStack上で管理されています。

f:id:nuitsjp:20170526175907p:plain

このため、逆にStackをPOP>POP>POPとすることで、画面を戻ることができます。

f:id:nuitsjp:20170526180003p:plain

さて、このStackですが2種類あります。
Modal StackとNavigation Stackです。それぞれのStackごとに、個別のPushとPopのメソッドがあります。
Stackは、いずれか選んで利用するというわけではなく組み合わせて利用する事もできます。そうすることで、自由な画面遷移を実現することができます。

f:id:nuitsjp:20170526180045p:plain

具体的に例を見ていきましょう。 ここではNavigationPageとContentPageのみを利用します。

まずNavigationPageをMainPageに設定します。 するとModal StackとNavigation Stackがひとつづつ作成されます。 MainPageはModalStackに含まれていないのがポイントです。

f:id:nuitsjp:20170526180158p:plain

ここでPushAsyncをよびだすと、NavigationStackにPageが追加され表示上もその画面に遷移します。 さらにPushAsyncをよぶと、三番目のPageに遷移しますね。

f:id:nuitsjp:20170526180313p:plain

ここでPushModalAsyncを呼び出してみましょう。
するとModalStack上に NavigationPageが設定され、新しいNavigation Stackがもう一つ増えます。

つまり、 Navigation StackはNavigationPageと 1:1で存在し、NavigationPageが増えればその分増えます。
それに対してModalStackはシステムに1つのみ存在することになります。

これらを組み合わせて比較的自由な画面遷移が構築できます。

f:id:nuitsjp:20170526180405p:plain

続いて戻るケースです。

f:id:nuitsjp:20170526180654p:plain

この状態で PopAsyncを呼ぶと、NavigationStackからページが一つ削除され、まえのページに戻ります。
下の図は2回PopAsyncを呼んだ結果です。

f:id:nuitsjp:20170526180740p:plain

つづいてPopModalAsyncを呼びます。ModalからPopするため、ModalStackからNavigationPageと、そのPageが管理しているNavigationStackも削除されます。
結果、2番目のNavigationStackの最後のページが表示された状態となります。

f:id:nuitsjp:20170526180906p:plain

それでは、この状態でPopModalAsyncを呼び出したらどうなるでしょう? そうです。ModalStackからNavigationPageが削除され、紐づくNavigationStackも削除されます。 結果、ひとつめのNavigationStackの最後のページが表示されます。 ウィザード形式の画面構成で、処理を途中で中断するような場合に使えるかもしれません。

f:id:nuitsjp:20170526181002p:plain

さて、最後にPopToRootAsyncも紹介ししておきましょう。 PopToRootAsyncこれを呼び出すと、 NavigationStackの最初のページまで巻き戻されます。

f:id:nuitsjp:20170526181048p:plain

ここまででModalStackとNavigationStackの概要についてご理解いただけたかと思います。 といっても、利用した画面はNavigationPageとContentPageだけです。 次はそれ以外のPageを交えた複合パターンについてお話ししたいと思います。

ここではMasterDetailPage を例に見ていきましょう。 ApplicationのMainPageにMasterDetailPageを設定する事を想定します。

f:id:nuitsjp:20170526181228p:plain

そして、MasterDetailPageのMasterつまりメニュー側に ContentPageをDetail側つまりメインコンテンツ側に TabbedPage を設定します。 TabbedPageには二つTabを設定し、それぞれに NavigationPageを設定したとしましょう。 そうした場合、このように、NavigationPageごとに独立したNavigationStackが構成されます。 もちろん独立して問題なく遷移管理できます。

ちなみに、NavigationStack以外の黒枠の部分は、ModalStackにも、NavigationStackにも含まれないStack管理外の領域になります。

f:id:nuitsjp:20170526183546p:plain

さて、これを踏まえて、アプリケーションを構築するにあたって画面遷移サービス構築に説明を移りたいと思います。

画面遷移サービスを構築する際、特に重要な要素が二つあると私は考えています。

f:id:nuitsjp:20170526181350p:plain

ひとつは、画面を実際に遷移させる機構です。 そしてもうひとつは、画面遷移のイベント通知機構です。

今回は後者、画面遷移のイベントをどう管理・通知するかについて考えたことをお伝えしたいと思います。

さて、ここから話を勧めるにあたって、この場での用語の統一を図っておきたいと思います。まずは前に画面遷移する際です。

f:id:nuitsjp:20170526181447p:plain

画面遷移するときに、画面から退出するさいにOnExitを、つぎの画面に入場する際にOnEntryを呼び出すこととします。

そして戻る時の画面遷移です。

f:id:nuitsjp:20170526181536p:plain

戻る時、元画面はリソースなどを開放して破棄するのでOnDestroyを、戻った先ではOnEntryを呼ぶことにします。

Forward時もBack時も入場のイベントがOnEntryになっています。 勿論分けても良いのですが、分ける意味があるケースは少ないと思いますので今回は一緒にしておきます。

そして、画面遷移するさいに、遷移イベントの通知を受けたい場合はこんなようなインターフェースを実装して必要な処理を行う事にしましょう。

f:id:nuitsjp:20170526181626p:plain

さて、画面遷移のイベント処理を実装するにあたり 最初にちょっと個人的に怖いと思っている方法についてお話ししておきます。

それはPageのAppearingやDisappearingイベントです。

f:id:nuitsjp:20170526181733p:plain

私も最初はそれで実装しましたが、後々困ったことが起こるケースがあります。

  • 退出と破棄のタイミングについて区別ができない
  • PopToRootAsyncやPopModalAsyncなど、複数ページまとめて処理しようとすると詰む
  • SleepやResume時にもイベントが発生するため切り分けることが難しい
  • プラットフォームによって挙動が異なる

というわけで、今回はこれらは封印する方向でいきます。

ではForwardの具体的な実現方法から行きましょう

f:id:nuitsjp:20170526181843p:plain

ForwardはINavigationのメソッドを呼び出さない限り行えません。
したがって、それらのメソッドをラップしてイベントを発生させてあげれば良いのでらくちんです。

f:id:nuitsjp:20170527131051p:plain

こんなかんじでしょうか?簡単でが、いくつか疑問に思われるところもあるかと思います。
ひとつは、遷移先の画面のOnEntryが遷移元のOnExitよりも先に呼ばれている点です。

OnEntryでは画面の初期描画処理を行います。
このため、Pushよりも先に実行しないと、画面を構築してるふるまいが利用者の目に映ってしまいます。

同様にOnExitでは描画リソースの開放処理を行います。
なのでPushした後に行わないと、画面の崩壊が利用者に見えてしまいます。

このため、こういう処理順が一般的には好ましいと考えています。

つづいてBack処理です。バックは辛いです・・・

f:id:nuitsjp:20170526182103p:plain

先ほど見ていただいた、この表のうちPopModalAsyncは対応は容易です。Pushと同様にメソッドをラップして好きに処理してください。
問題はNavigationPageの戻る処理です。
辛い理由は大きく二つに分けることができます。

f:id:nuitsjp:20170526182210p:plain

まずそもそも、直前のページに戻る手段が3種類あります。
しかもこのうち、PopAsync以外は、ユーザー操作に基づいて、Formsが勝手に処理してくれてしまいます。 なので、メソッドをラップする方法を使えません。

どうしましょう?

ってことで、最も簡便なのはNavigationPageのPoppedイベントを利用することです。 それ以外の方法はCustomRendererと仲良くなる必要があります。

ここではPoppedイベントを利用する方法を紹介しましょう。

こんな感じです。

f:id:nuitsjp:20170526182318p:plain

まずはNavigationPageをカスタマイズして、Poppedイベントを購読します。

Poppedが発生したら、NavigationPageのCurrentPageにOnEntryを通知し破棄されるページにOnDestroyを通知してあげます。

さて、実は一つ問題があります。
というのは、PoppedイベントはBack処理が完了して、前画面が表示された後に呼び出されるということです。 つまり画面表示の後にOnEntryが呼び出されるということです。

これはけっこう根深い問題で、実はXamarinのバグジラにこれをスマートに解決する方法を提供してほしいという要望が半年から1年くらいほど前に挙げられているんですが対処できていません。

これは特に、Swipeの扱いが難しい点に理由あるのではと考えています。
Swipeの場合、戻ろうかな~やっぱや~めたみたいなことができてしまうので考えただけでつらいですよね。

さて、単一のアプリを作るだけならこれで良いのでしょうけどこの方法だと再利用性がないに等しいです。 NavigationPageの数だけ同じ実装が必要になります。

そこで、Prismで利用されている非常に面白いアイディアがありますのでここで紹介したいと思います。 ちょっとこれを見た時は、その発想に感動しました。別にPrism発祥ではないかもしれませんけど。

f:id:nuitsjp:20170526182411p:plain

まずはPoppedに対する処理をBehaviorに切り出します。 こんな感じでしょうか。 ページの都合上、デタッチを書いていませんが、実際に作る時は当然デタッチの実装を忘れないようにしてください。 メモリリークしちゃいますので。

BehaviorをNavigationPageにAttachして、その中でPoppedイベントを購読します。 ここは普通ですね。 面白いのはこれを設定する方法です。

f:id:nuitsjp:20170526182508p:plain

どこで設定するかと言うと、画面遷移サービスです。 画面遷移するときに、遷移先の画面のベースクラスを判定して、 Behaviorをインジェクションしています。 このデザインパターンは非常に再利用性高いと思います。

つづいて面倒な理由その2。PopToRootAsyncです。

f:id:nuitsjp:20170526182618p:plain

こやつ、Poppedイベントに該当する、PoppedToRootイベントがあります。 あるんですが、このメソッド呼ぶと複数ページが纏めてPopさるのに、何のページがPopされたのか教えてくれません。

ただ、PoppedToRootAsyncも、メソッド呼び出しでしか実行できませんので ラップメソッドで実現してあげればよいと思います。

f:id:nuitsjp:20170526182709p:plain

こんな感じです。 ポイントは幾つかあります。

あとあと、POPされた全てのページのOnDestroyを呼び出してあげたいんですが PopToRootAsyncを呼び出すと、そのタイミングでNavigationStackが全てクリアされてしまいます。 なので、最初にStackをコピーしておいてあげます。

また、PopToRootAsyncされたあと、NavigationPageのCurrentPageにたいして OnEntryを通知する必要があります。 NavigationPageはfromのParentから取得できますが、これもPopToRootAsyncを呼び出すと クリアされてしまいますので、先にローカル変数へ確保しておく必要があります。

あとはまぁ適当にちょいちょいとすればOKです。

さて、NavigationPage以外についても簡単に触れておきましょう。

f:id:nuitsjp:20170526182800p:plain

MasterDetailに関しては、MasterとDetailのプロパティを入れ替える実装になるので、そこで頑張れば行けるでしょう。 TabbedPageやCarouselPageは、CurrentPageChangedやPagesChangedイベントを利用して 先ほどのBehaviorをインジェクションするパターンで実現するといいでしょう。

ちょっと注意してほしい点が一つあって、これらのページは、子ページを直接XAML上に書けるわけですが、そのケースは、そうじゃないケースと同一に扱えない可能性があるので気を付けてください。

最後に、簡単にまとめましょう。

f:id:nuitsjp:20170526182856p:plain

という訳で、発表内容はおおよそ以上です。
何か質問があれば、コメントなりTwitterなりいただけれければ、お話しできるかと思います。

以上!疲れた!