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

Makopy'5 La6

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

(Xamarin.Forms)アプリを終了させる

Xamarin.Forms

スマホアプリにおけるアプリ終了

基本的にあまりしたくないことではありますが…。
実戦のアプリ開発では、
「アプリを終了させたい 」
というシーンや要求に出くわすことがあります。

iOSの場合

可能かどうかと問われれば、可能です。
exit()をコールすればアプリは終了します。
しかしながらiOSにおいて、
プログラミングによりアプリ終了をしてよいかどうか?
については、諸説あります。

iOS Human Interface Guidelines」に下記の記載があります。

iOSアプリケーションをプログラム的に強制終了させることは避ける。  
単にクラッシュしたものと受け取られる可能性があります。  
何らかの原因でアプリケーションが意図通り機能しない場合は、その状況を伝え、何ができるかを説明しなければなりません。

iOSヒューマンインターフェイスガイドライン: 起動と停止

iOSの公式のドキュメントで上記のように言及されていることもあり、
iOSではアプリを終了してはいけない」
という論調が多く見られます。
しかしながら、
「いきなり終わって、不正終了みたくなるのがよくない」
と言っているわけであって、
「理由を表示した上でアプリを終了するならよい」
という解釈もあり、筆者はこちら派です。
どちらが真に正しいかは判断しかねますが、実際にexit()をしているアプリも見かけます。
exit()そのものがリジェクトに直結するわけではないと考えられます。

上述のような経緯から、
iOSでアプリ終了に持ち込みたい場合、

  • 何かしらメッセージを表示した上でexit()する
  • 再起動を促すメッセージを表示した上で、ユーザーが何もできない状態にする
    • ユーザーは再起動以外できることがない

という2つの選択肢があります。

Androidの場合

Androidについては、
「プログラミングによりアプリ終了をしてよいかどうか? 」
に関する議論はあまり見たことがない気がします。

アプリ終了の具体的な実現方法については、いくつかあります。
下記の記事が詳しくまとめてくれています。
www.ecoop.net

Xamarin.Formsにおけるアプリ終了

System.Diagnostics.Process.GetCurrentProcess().CloseMainWindow()

ググってみると、

System.Diagnostics.Process.GetCurrentProcess().CloseMainWindow()
を使えばよさそうなことが分かりました。 しかしながら共通実装部からは使えません。
使うためにはNuGetパッケージを追加しなければならないのですが、 共通実装部ではパッケージの追加もできません。

(補足)
この点については環境を含めた筆者の認識・理解不足があるかもしれません。  
少なくとも手持ちの環境(Mac OS上のXamarin Studio)ではパッケージ追加できませんでした。  
また、System.Diagnostics.Processとは、本来共通実装部で使用すべきでない/できないものなのかもしれません。  
気持ち悪いですが、ここでは深追いせずに、手持ちで出来る範囲で実現した方法を記載しておきます。  

さて、共通実装部ではできなくても、
各ネイティブ(.Droid/.iOS)にはパッケージを追加して、利用することができます。
ので、今回はDependencyServiceとして実装することにしました。

Xamarin.Formsでアプリ終了を実現するコード

DependencyServiceにより実現

  • 共通実装部
  • .Droid

全く同じメソッドを呼んで同じ処理をするのにDependencyServiceにするのはちょっと納得いかないですが…。
強いて言えば、Androidでのアプリ終了はいくつかの方法があるので、実情に応じて別の実装に置き換えてもいいと思います。
例えば、

  • .Droid

とすることもできます。

今回実験をしたサンプルコードはこちら。
github.com

相変わらず実験用の適当なコードです…。

Xamarin.FormsにおけるMVVM

はじめに

本記事は、個人的な理解と見解を基に記述しています。
MVVMについて、本質を正しく・深く理解することを目的とした場合には、こことは別な記事を参照された方が良いと思います。
ここでの狙いは、Xamarin.Formsでの開発にどうしてもつきまとうMVVMという考え方を、まずはなるべく最小限の理解で乗り切ろうという発想です。

UIソフトウェアアーキテクチャパターン

以降は「UIパターン」と呼ぶことにします。
いろいろありますが、有名どころで言うと下記のようなものがあります。

  • MVC(Model-View-Controller)
  • MVP(Model-View-Presenter)
  • MVW(Model-View-Whatever)
  • MVVM(Model-View-ViewModel)

Xamarin.FormsはMVVMだと言われています。

UIパターンを理解するための考え方

上記のようなUIパターンには、それぞれの背景などがあるわけですが。
共通して言えることは、
「関心の分離」
ということだと思います。
すなわち、適切な役割分担をすることで、
クラスやモジュール間の結合を疎にしたい
という意図がまず第一にあると考えます。

一定規模以上のアプリ開発においては、
V、すなわちUIビジネスロジックの部分の分離
については、そうした方がいいのはある程度誰にでも自明のことであり、
何かしらの手段により分離が実現されていることが多いと思います。

だから、いくつかあるUIソフトウェアアーキテクチャについては、

  • まずV(UI)をどのような手法で実装すべきか/できるか?
  • V(UI)の実装のあり方から逆算してビジネスロジック部をどう分離し、繋ぎこむのがよいか?

という捉え方をした上で、
開発言語/SDK/フレームワークの特性を加味していくと、ある程度概形が見えてくるのではないかと思っています。

Xamarin.FormsにおけるMVVM

「Xamarin.FormsはMVVM」と言われる大きな要因として、XAMLの存在があります。

Xamarin.Formsにおいては、V(UI)の構築にXAMLを使用することになります。
(コードビハインドにより実装することもできますが、多くの場合XAMLを使用すると思います。)

XAMLに対して値を動的に設定したい場合はBindingが発生します。
XAMLのBinding対象となる裏方というか、影のような存在が必要です。
それがまさにVM(ViewModel)の存在です。
XAMLへのBinding対象となる存在をViewModelという呼び方をしましょう。
Viewそのものではないので、Viewと異なる関心として切り離しておきましょう。
上記の仕組みから、見た目の部分のつながりはView-ViewModelによって成り立つことになります。
その上で、ビジネスロジックは見た目から分離させた方がいいので、その部分をModelに押し込みましょう。
強引かもしれませんが、これでMVVMとなると思います。

最初の一歩を躓かないためには、多少強引にでも概形を捉えておくことが大事だと思います。

すっぴんのXamarin.FormsでMVVM

プロジェクトを作成したときのデフォルトの状態では、MVVMの形を取っていません。
最初のスケルトンがV-VMの形を取ってくれているといいのですが、そうはなっていませんね。
最初の画面がXAMLC#のセットで出来てきます。
これはVの部分だけです。
「Xamarin.FormsはMVVMだ」
とは言うものの、ViewModelはデフォルトでは存在しないのです。
加えて、はじめの一歩のちょっとした実装をしてみるには、Pageにコードビハインドして実装しても事足りてしまったりします。

Xamarin.FormsでMVVMをやろうとして、ViewModelを作ってみようと思って調べてみると…。
下記のようなINotifyPropertyChangedを継承したベースを作って、ViewModelはこれを継承する形とするのが望ましいと…。

すなわち、

  • ケルトンを作る
  • BindableBaseを作る
  • BindableBaseを継承してViewModelを作る
  • 作ったViewModelBindingContextにする

とまでやって初めて、 V-VMの部分が成り立つわけです…。
すっぴんのXamarin.FormsでMVVMというのは、意外に面倒なものです。

やはり素直に何者かの助けを借りた方がいい

仕組みを理解するためにも、一度くらいは上記の手間を踏んでやってみた方がいいですが…。
コピペとは言え、いつもBindableBaseを追加してどうこうは面倒でしかないですね。
Xamarin.FormsにおけるMVVMについては、何者かの助けを借りた方がよさそうです!

有名どころでは、

  • Prism for Xamarin.Forms
  • MVVM Light Toolkit

があるようです。
個人的には、Prism.Formsを利用してみようと思っています!

前回記事でも紹介しましたが。
Prism.Formsに関しては、nuits.jpさんがきちんとまとめて下さっていて、非常に参考になります! www.nuits.jp

(Mac)Xamarin StudioにPrism Template Packを導入する

Xamarin.Forms

導入手順

導入自体は簡単です。

アドインマネージャーを起動

f:id:makopy_inside:20170316092702p:plain

アドインマネージャーから選択してインストール

検索バーに「prism」と入力すると候補に出てくる
f:id:makopy_inside:20170316092709p:plain

利用手順

新規プロジェクト作成時に「Prism Unity App」を選択

f:id:makopy_inside:20170316092738p:plain

新規プロジェクトが出来たらそのままスケルトンをビルドしたいところですが…。
そのままだとエラーになってしまいます。

usingの追加

.iOSのAppDelegate.csと.DroidのMainActivity.csに下記のusingを追加する。

ここまでやるとスケルトンがビルドできます。


あとがき

本来なら、Prismとは何ぞやとかMVVMについてとか、自分自身のために整理しておきたいところですが…。
その辺りはまた暇を見て書けたらなぁと思います。

Prismについては、nuits.jpさんがきちんとまとめて下さっていて、非常に参考になります! www.nuits.jp

Prismを使用するメリットはたくさんあります。
本当は、もう少しすっぴんのXamarin.Formsで苦労をしてからその恩恵を受けようと思っていましたが…。
便利なので必要に応じてガンガン使っちゃいます!w

(Xamarin.Forms)TabbedPage

Xamarin.Forms

(以前の記事)
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キー押下に対応する

Xamarin.Forms

Backキー

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

Backキー押下への対応方法

OnBackButtonPressed - Xamarin

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

注意事項

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

Backキーの無効化

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

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

(Xamarin.Forms)Modal Pages

Xamarin.Forms

(以前の記事)
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

Xamarin.Forms

前回のお話。
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