読者です 読者をやめる 読者になる 読者になる

Makopy'5 La6

スマホアプリ開発とかその周辺のこととか関係ないこと。または恋は言ってみりゃボディー・ブロー

(Xamarin.Forms)TabbedPage

(以前の記事)
Xamarin.Formsの画面遷移 - Makopy'5 La6

特に理解しておくべき画面遷移として、下記を挙げています。

  • Hierarchical Navigation
  • Modal Pages
  • TabbedPage

これまで、

にスポットを当ててきました。

今回はTabbedPageにスポットを当ててみたいと思います。


TabbedPageのポイント

Xamarinでのアプリ開発は日が浅いのであまり実戦事例が足りていませんが…。
スマホアプリ開発として見ると、実戦の開発では、Tabによる画面(機能)の切り替えは遭遇率が高いです。
画面(機能)が多岐に渡る場合はMaster-Detail Pageもありえますが。

例のごとく、ベーシックな知識は公式のDevelopersガイドから得るのが一番です。
developer.xamarin.com

画面管理

  • TabbedPageの子要素(Children)にPageを設定する
  • ItemsSource, ItemTemplateを使用した画面設定も可能

ItemsSource, ItemTemplateを使う方法は同一構成の複数画面が前提なので、個人的にはCarousel Pageなイメージです。 また、
「機能的に異なるいくつかの画面 + 設定画面」
みたいなのが、Tabを使ったアプリには多い気がします。
そのような場合は画面単位に実装を担当する可能性が高く、基本的にはChildrenにPageを設定するような実装をまずは押さえておくべきだと思います。

Tab構成に関する注意点

  • 子要素はNavigationPage, ContentPageインスタンスで構成されるべき
  • 含まれる子要素が多すぎるとパフォーマンスが影響を受ける可能性がある
  • TabbedPageNavigationPageに配置するのは非推奨
    • iOSではUITabBarControllerUINavigationControllerのラッパーとして動作するため

UI関連

  • TabのタイトルはPageのTitleが設定される
  • Tab上のボタン用に画像が必要
  • Tabの表示位置

Tab表示位置に関する悩み…

上述の
「いくつかの画面 + 設定画面」
のパターンを考えてみましょう。
ListViewを設定画面トップとして、各セルタップで子画面をPushAsync
と、やりたくなるのですが…。
Android, WindowsではTabが画面上部に表示されます。
Tabの子要素にNavigationPageを設定すると、NavigationBarがTabのさらに上に表示されてしまいます…。

NavigationBarを非表示にする方法

(参考)

ytabuchi.hatenablog.com

XAMLなら、

C#なら、

NavigationBarを非表示にすると…

NavigationBarを非表示にするとなると、また別の悩みが出てきます。
Backキーを持たないiOS用にわざわざ戻るUIを実装しなければならない。
実装するにしても、Tabの表示位置の違いを意識して配置することになり、非常に悩みます。

個人的に現時点での落としどころとしては…。

  • iOSではNavigationBarを表示する
  • Android, Windowsではナビゲーションバーを非表示にしてBackキーで戻るようにする

その他もろもろ

表示画面変更の捕捉

OnCurrentPageChangedをオーバーライドする

表示画面を切り替える

Currentを変更する

サンプル

TabbedPageについて、サンプル実装してみました。
いつもながら実験用の適当なコードですが…。

github.com

(Xamarin.Forms)Backキー押下に対応する

Backキー

Androidにあるハードキーの戻るボタンです。
扱ったことがないのでよく知らないのですが、Windows Phoneにもあるみたいです。

Backキー押下への対応方法

OnBackButtonPressed - Xamarin

Page.OnBackButtonPressedをオーバーライドすることで対応できます。
iOSではBackキー押下のイベント自体が上がってきません。

注意事項

オーバーライドして対応を入れた場合、メソッドの戻り値としてtrueを返すべき。
うっかりfalseを返してしまうと…。
アプリがBackキー押下に反応しなかったとシステムに判断されてスタート画面やホーム画面に戻されてしまいます。

Backキーの無効化

Backキーを無効化する為には、下記のようにすればよい。

何もせずにtrueを返すことで、事実上バックキーを無効化することができます。
ここでもうっかりfalseを返すと…。
上記の「注意事項」に記載の通り、意図せずスタート画面やホーム画面に戻されてしまいます。

(Xamarin.Forms)Modal Pages

(以前の記事)
Xamarin.Formsの画面遷移 - Makopy'5 La6

特に理解しておくべき画面遷移として、下記を挙げています。

  • Hierarchical Navigation
  • Modal Pages
  • TabbedPage

前回はHierarchical Navigationにスポットを当てました。

makopy5la6.hatenadiary.jp

今回はModal Pagesにスポットを当ててみたいと思います。


Modal Pagesのポイント

やはり、ベーシックな知識は公式のDevelopersガイドから得るのが一番です。
Modal Pages - Xamarin

画面遷移の管理

  • ModalStackで管理
  • ModalStackはNavigationPageを必要としない
    • 任意のPage派生クラスでOK
  • 画面を表示するときはModalStackにPush
    • Navigation.PushModalAsync
  • 画面を閉じるときはModalStackからPop
    • Navigation.PopModalAsync

ModalStackの操作

  • ModalStackは操作できない
    • InsertPageBeforeRemovePageのような操作が存在しない
  • PopToRootAsyncはサポートされていない

ModalStackの取得

Page内でなら、下記の様に取得できます。

PageのList(ReadOnly)が取得できます。

ModalStack - Xamarin

UI関連

  • 基本的にNavigationBarを伴わない
    • NavigationPageをPushModalAsyncした場合は表示される
      • ただし、戻るボタンはサポートされない
      • 画面を閉じるためのUIは自前で実装しなければならない

その他

  • Backキーを無効にすべき
    • Android/Windows Phone
    • 「モーダル」の意味合いから、何かしらの操作/処理の完了後に画面が閉じられるべき

Backキー無効については、別記事にて整理予定です。

Modal Pages/Hierarchical Navigationと画面ライフサイクル

下記のサンプルコードで確認してみました(iOS/Android)

github.com

相変わらず、動作確認用のどうしようもないコードです…。

Modal PagesとHierarchical Navigationを複合しています。

  • TopPage→Page1→Page2→Page3→NavigationRootPageとPushModalAsync
  • NavigationRootPage→PushPage1→PushPage2→PushPage3とPushAsync
  • PushPage3→PushPage2→PushPage1→NavigationRootPageとPopAsync
  • NavigationRootPage→Page3→Page2→Page1→TopPageとPopModalAsync
  • 各画面のOnAppearingOnDisappearingでログ出力

確認まとめ

  • PushModalAsync, PopModalAsync
    • iOS
      • 遷移先画面のOnAppearing → 遷移元画面のOnDisappearing
    • Android
      • 遷移元画面のOnDisappearing → 遷移先画面のOnAppearing

iOSでの呼ばれる順番に注意!
呼ばれる順番性を当てにした実装はしてはいけませんね。

おまけの確認(ModalStackとNavigationStack)

各画面のOnAppearingOnDisappearingで、NavigationStackとModalStackの中身をログ出力しました。

  • ContentPageをPushModalAsync
    • ModalStackにのみ積まれて、 NavigationStackはカラ
  • NavigationPageをPushModalAsync
    • ModalStackとNavigationStack両方にNavigationPageが積まれる
    • このときのログ出力
      • NavigationStack
        • 具体的な名前が表示される
          • ex) “ModalPagesSample.NavigationRootPage”
      • ModalStack
        • NavigationPageとして表示される
          • “Xamarin.Forms.NavigationPage”

ModalStackとNavigationStack

公式のDevelopersガイドで図解してくれていますが、図だけ見るとほとんど概念上の差がありません…。
Hierarchical Navigation - Xamarin
Modal Pages - Xamarin

ということで…。
サンプル実装と確認結果を踏まえて、個人的な整理として図式化してみました。
(ちなみに、筆者の作図能力は著しく低いです…)

(Xamarin.Forms)Hierarchical Navigation

前回のお話。
makopy5la6.hatenadiary.jp

画面遷移のことを書きましたが。
特に理解しておくべき画面遷移として、下記を挙げました。

  • Hierarchical Navigation
  • Modal Pages
  • TabbedPage

今回はHierarchical Navigationにスポットを当ててみたいと思います。

Hierarchical Navigationのポイント

兎にも角にも、まずはベーシックな知識は公式のDevelopersガイドから得るのが一番です。
Hierarchical Navigation - Xamarin

画面遷移の管理

  • NavigationStackで管理
  • NavigationStackのRootはNavigationPageとする
  • 次の画面に行くときはNavigationStackにPush
    • Navigation.PushAsync
  • 前の画面に戻るときはNavigationStackからPop
    • Navigation.PushAsync
  • Root画面までPopできる
    • Navigation.PopToRootAsync

NavigationStackの操作

  • NavigationStack内に画面を挿入
    • Navigation.InsertPageBefore
  • NavigationStack内から画面を削除
    • Navigation.RemovePage

あくまで私感でしかありませんが…。
この手の操作はあまり好きになれません。
PopToRootAsyncも含めて、Pop/Push以外の処理を書かねばならない場合、
「そもそも画面遷移仕様がきちんと整理されているのか?」
「仕様はともかく、実装面で他の方法はないのか?」
という風に疑うべきだと思います。

またこの様な場合、
「特定の条件やシーンに応じて画面遷移に変更を加えたい」
というようなパターンが多いと思いますが、仕様面の都合はNavigationStackでは管理してもらえません。
なるべくベーシックで自然な画面遷移に収めた方が安心です。

とは言え…。
「事実は小説より奇なり」
本番の開発では諸般の都合でトリッキーな画面遷移を書かざるを得ない時があるワケです。
こういう特殊な操作があることは覚えておいて、やむを得ない時は十分注意して使うというくらいのスタンスを取りたいと思います。

NavigationStackの取得

Page内でなら、下記の様に取得できます。

PageのList(ReadOnly)が取得できます。
NavigationStack - Xamarin

ReadOnlyなので、直接NavigationStackの操作ができるわけではありませんが。
ログ出ししたり、操作メソッドの引数に設定するPageを取得したりという用途で使えると思います。

UI関連

  • 基本的にNavigationBarを伴う
  • NavigationBarのTitleにはPageのTitleが採用される
    • 基本的にはこのTitleが次の画面の戻るボタンのTitleになる
    • Pageのタイトル

画面のライフサイクル

画面遷移に伴わせて、どんなライフサイクルメソッドが呼ばれるかを検証したかったのですが…。
Xamarin.Formsの画面ライフサイクルって…

  • OnAppearing
  • OnDisappearing

の2つしかなくて、 基本的には画面の表示/非表示しか捕捉できません。

Hierarchical Navigationと画面ライフサイクル

下記のサンプルコードで確認してみました(iOS/Android)

github.com

動作確認用のどうしようもないコードですが…。

  • TopPage→Page1→Page2→Page3とPushAsync
  • Page3→Page2→Page1→TopPageとPopAsync
  • Page3からはPopToRootAsync, InsertPageBefore, RemovePageも試しています。
  • 各画面のOnAppearingOnDisappearing でログ出力

確認まとめ

iOSAndroidで同じ結果

  • PushAsync, PopAsync
    • 遷移元画面のOnDisappearing → 遷移先画面のOnAppearing
  • PopToRootAsync
    • 遷移元画面のOnDisappearing → 遷移先画面のOnAppearing
  • InsertPageBefore
    • ライフサイクルメソッドは呼ばれない
  • RemovePage
    • ライフサイクルメソッドは呼ばれない

当然ながら、画面の表示/非表示が発生しなければ、OnAppearingOnDisappearingは呼ばれない。

おまけの確認

iOSAndroidで同じ結果

各画面のOnAppearingOnDisappearingのタイミングで、併せてNavigationStackの中身をログ出力していたのですが…。

  • PopToRootAsyncした場合、NavigationStackが一度空になってからTopPageが積まれる
  Page3.xaml.cs - OnDisappearing
  ***** NavigationStack *****

  TopPage.xaml.cs - OnAppearing
  ***** NavigationStack *****
  HierarchicalNavigationSample.TopPage

Xamarin.Formsの画面遷移

前回、アプリのライフサイクルについて整理しました。

makopy5la6.hatenadiary.jp

今回は画面のライフサイクルについて整理しようと思いましたが…。
その前に画面遷移についてさらっと整理をしておきたくなりました。

Xamarin.Formsの画面遷移のパターン

公式のDevelopersガイドによれば、 Xamarin.Formsで用意された画面遷移のパターンは下記の通りです。

  • Hierarchical Navigation
  • TabbedPage
  • CarouselPage
  • MasterDetailPage
  • Modal Pages
  • Displaying Pop-Ups

(参考)
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/navigation/

iOSエンジニア的には捉えやすい分類になっていますね☆

画面遷移は超重要!

実戦の開発では、画面遷移は非常に重要です。
開発を進めるにあたっての要件定義や設計の進め方は色々あると思います。
その中で個人的にもっとも重視しているのは、
「早いうちに画面を洗い出し、画面遷移を決めにかかる」
ということです。
それがそのアプリの骨組みになります。

場合によっては、
その骨組みを成り立たせるために、

  • 特殊な画面遷移制御が必要なのか
  • 必要だとすればどう実現すればいいのか

という検討が必要です。

画面遷移そのものの検討にも、
特殊な場合の検討にも、
ベーシックな画面遷移の理解が必須だと思っています。

特に理解しておくべきと思う画面遷移

下記の3つは特に理解しておくべきと思います。

  • Hierarchical Navigation
  • Modal Pages
  • TabbedPage

どれも頻出の画面遷移です。
いろんなアプリがあるので一概に言えませんが…。
この3つの適切な組み合わせで対応できるケースは少なくないと思います。

加えて。
これらの画面遷移パターンに絡めて、画面のライフサイクルを押さえておくことも重要だと思います。

ということで。
画面のライフサイクルについては、画面遷移パターンに絡めて整理して記事にしていこうと思います!

「基礎の基礎が怖いってことを、今日何度も言っておきます」

というようなセリフを何かのCMで聞いたことがあります。
スマホアプリ開発において、
ライフサイクルや画面遷移は基礎の基礎ではないかなぁと思います。
本番の開発でも、ここの理解・認識・検討を誤ると大変なことになります。
だから、分かってるつもりだけど、敢えて整理をして覚書をしておこうと思ったりもするわけです。

(Xamarin.Forms)アプリのライフサイクル

ライフサイクルの捕捉

ライフサイクルの捕捉は、スマホアプリ開発において非常に重要です。
機能要件や実装都合等の事情により、
ライフサイクルのタイミングに合わせて処理をする。
ということがしばしばあるからです。

ライフサイクルは大きく2つに分けられます。

  • アプリのライフサイクル
  • 画面のライフサイクル

今回は「アプリのライフサイクル」について書きます!

Xamarin.Formsにおけるアプリのライフサイクル

公式のDevelopersガイドに記載があります。
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/app-lifecycle/

Applicationクラスには、以下の3つのライフサイクルメソッドが存在します。

  • OnStart
    • アプリケーションの起動時に呼び出される
  • OnSleep
    • アプリケーションがバックグラウンドに移動するたびに呼び出される
  • OnResume
    • アプリケーションがバックグラウンド遷移後、再開されたときに呼び出される

泣いても笑ってもこの3つしかありません。
以下ではこの3つのライフサイクルメソッドが呼び出されるタイミングを調査します。

呼び出されるタイミングについて調査

  • Appクラス
  • .DroidのMainActivity.cs
  • .iOSのAppDelegate.cs

のそれぞれのライフサイクルメソッドにログを張って、

  • 起動
  • バックグラウンド
  • フォアグラウンド

に関連しそうな操作をしてみました。

調査そのものに関係ないのですが…。
.iOSのAppDelegateのライフサイクルメソッド名がネイティブとだいぶイメージが違うなぁと思いましたw
iOSネイティブが大好物な人なので、違和感ありありですw

調査結果から見たポイント

調査ログを延々と記載する前に…。
先に結論というかポイントをまとめておきますw

OnResumeの注意点

OnResumeAndroidにおけるOnResumeと違って、起動時は呼ばれない

OnSleep/OnResumeの注意点

iOSでは、本来バックグラウンドと見なしたくない下記のタイミングで、OnSleep/OnResumeが呼ばれてしまう。

  • 通知センターの表示/非表示
  • コントロールセンターの表示/非表示

実際にはバックグラウンド/フォアグラウンドではなく、アクティブ/インアクティブに紐づいてしまっている。
従って、
OnSleepが呼ばれるタイミングは必ずしもバックグラウンドではない


ということで、以下調査ログの記録です。

iOS

起動時

  • AppのOnDestroyがAppDelegateのWillFinishLaunchingの直後に呼ばれる
AppDelegate.cs - WillFinishLaunching
AppDelegate.cs - FinishedLaunching
App.xaml.cs - .ctor
App.xaml.cs - OnStart
AppDelegate.cs - OnActivated

ホームボタン押下時

  • AppのOnDestroyがAppDelegateのOnResignActivationの直後に呼ばれる
AppDelegate.cs - OnResignActivation
App.xaml.cs - OnSleep
AppDelegate.cs - DidEnterBackground

ホームボタン押下後、マルチタスク画面から選択

  • AppのOnResumeがAppDelegateのOnActivatedの直後に呼ばれる
AppDelegate.cs - WillEnterForeground
AppDelegate.cs - OnActivated
App.xaml.cs - OnResume

ホームボタン押下後、アプリアイコンから起動

  • AppのOnResumeがAppDelegateのOnActivatedの直後に呼ばれる
AppDelegate.cs - WillEnterForeground
AppDelegate.cs - OnActivated
App.xaml.cs - OnResume

マルチタスク画面表示時

  • AppのOnSleepがAppDelegateのOnResignActivationの直後に呼ばれる
AppDelegate.cs - OnResignActivation
App.xaml.cs - OnSleep

マルチタスク画面から再選択

  • AppのOnSleepがAppDelegateのOnResignActivationの直後に呼ばれる
AppDelegate.cs - OnActivated
App.xaml.cs - OnResume

通知センター表示

  • AppのOnSleepがAppDelegateのOnResignActivationの直後に呼ばれる
AppDelegate.cs - OnResignActivation
App.xaml.cs - OnSleep

通知センター非表示

  • AppのOnResumeがAppDelegateのOnActivatedの直後に呼ばれる
AppDelegate.cs - OnActivated
App.xaml.cs - OnResume

コントロールセンター表示

  • AppのOnSleepがAppDelegateのOnResignActivationの直後に呼ばれる
AppDelegate.cs - OnResignActivation
App.xaml.cs - OnSleep

コントロールセンター非表示

  • AppのOnResumeがAppDelegateのOnActivatedの直後に呼ばれる
AppDelegate.cs - OnActivated
App.xaml.cs - OnResume

マルチタスク画面からアプリKill

  • AppDelegateのDidEnterBackgroundWillTerminateが呼ばれる
  • Appのライフサイクルメソッドは呼ばれない
AppDelegate.cs - DidEnterBackground
AppDelegate.cs - WillTerminate

Android

アプリ起動時

  • AppのOnStartがActivityのOnStartの直後に呼ばれる
  • ActivityのOnResumeは呼ばれるが、AppのOnResumeは呼ばれない
MainActivity.cs - OnCreate
App.xaml.cs - .ctor
MainActivity.cs - OnStart
App.xaml.cs - OnStart
MainActivity.cs - OnPostCreate
MainActivity.cs - OnResume
MainActivity.cs - OnPostResume

ホームボタン押下時

  • AppのOnSleepがActivityのOnStopの直後に呼ばれる
MainActivity.cs - OnUserLeaveHint
MainActivity.cs - OnPause
MainActivity.cs - OnSaveInstanceState
MainActivity.cs - OnStop
App.xaml.cs - OnSleep

ホームボタン押下後、アプリ一覧から選択

  • AppのOnResumeがActivityのOnRestartの直後に呼ばれる
MainActivity.cs - OnRestart
App.xaml.cs - OnResume
MainActivity.cs - OnStart
MainActivity.cs - OnResume
MainActivity.cs - OnPostResume

ホームボタン押下後、アプリアイコンから起動

  • AppのOnStartがActivityのOnStartの直後に呼ばれる
  • ActivityのOnResumeは呼ばれるが、AppのOnResumeは呼ばれない
MainActivity.cs - OnCreate
App.xaml.cs - .ctor
MainActivity.cs - OnStart
App.xaml.cs - OnStart
MainActivity.cs - OnPostCreate
MainActivity.cs - OnResume
MainActivity.cs - OnPostResume

マルチタスクボタン押下時

  • AppのOnSleepがActivityのOnStopの直後に呼ばれる
MainActivity.cs - OnUserLeaveHint
MainActivity.cs - OnPause
MainActivity.cs - OnSaveInstanceState
MainActivity.cs - OnStop
App.xaml.cs - OnSleep

マルチタスクボタン押下後、アプリ一覧から再選択

  • AppのOnResumeがActivityのOnRestartの直後に呼ばれる
MainActivity.cs - OnRestart
App.xaml.cs - OnResume
MainActivity.cs - OnStart
MainActivity.cs - OnResume
MainActivity.cs - OnPostResume

通知バー表示

  • 何も呼ばれない

アプリ一覧からアプリKill

  • ActivityのOnDestroyが呼ばれる
  • Appのライフサイクルメソッドは呼ばれない
MainActivity.cs - OnDestroy

(Xamarin.Forms)ログにメソッド名を出力する

ログにメソッド名を出力したい!

そう思うことはちょいちょいあります。
iOSネイティブだと、

  • Objective-Cなら__PRETTY_FUNCTION__
  • Swiftなら#function

で、手軽にできるんですが。

Xamarin.Formsではどうやるんでしょうか?
ということで、調べて見ました。

MethodBase.GetCurrentMethod

パッと調べてみた感じ、
MethodBase.GetCurrentMethod
を使えばよさそう!

GetCurrentMethod - Xamarin

と思いましたが、
Xamarin.Forms(PCL)ではちょっと無理っぽい…。

MethodBase does not contain GetCurrentMethod in Portable Class Library (VS 2015)

Caller Info属性

もうちょっと調べてみたら、
Caller Info属性
というのがあるらしい(C# 5.0〜)

C# 5.0 の新機能 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

こいつならXamarin.Formsでもいけそう! ということで、 Caller Infoを使ったサンプル実装してみました!

とは言っても、
ほとんど上記のページの通りの実装に、 自分が欲しい形を追加しただけなんですが…。

Traceクラスを実装してみた

ログ出力周りについては、
実戦の開発で使うならいろいろ考慮せねばなりませんが…。
ここではベーシックにやり方を押さえるサンプルとして。

おまけ

iOS(Objective-C)の場合

iOS(Swift)の場合

Android(Java)の場合