前回の投稿 でとりあえず各プラットフォームでのバックグラウンド遷移の通知の仕方がわかったので、Xamarin.Forms の画面側に Suspending が通知されるようにしたいと思います
普通に App クラスにイベントハンドラを追加してもよいですが、せっかくなのでイベントの発信から受信を仲介する Prism の EventAggregator というライブラリを使っていこうと思います
もちろんライセンスは Apache 2.0 で依存関係なしなので Xamarin でも使えます!
いつものように Nuget パッケージマネージャを開いて Prism で検索すると Prism.PubSubEvents というライブラリがヒットするのでこれを Xamarin.Forms と各プラットフォームプロジェクトにインストールします
まずは EventAggregator でハンドリングするイベント関係のクラスを追加します
/// <summary> /// アプリケーション実行状態 /// </summary> public enum AppState { /// <summary> /// 未起動 /// </summary> NotRunning = 0, /// <summary> /// 実行中 /// </summary> Running = 10, /// <summary> /// 中断中 /// </summary> Suspended = 20, /// <summary> /// シャットダウン /// </summary> Terminated = 30, } /// <summary> /// iOS アプリケーション実行状態 /// </summary> public enum AppStateIOS { /// <summary> /// 未起動 /// </summary> NotRunning = 0, /// <summary> /// 非表示中 /// </summary> InActive = 5, /// <summary> /// 利用中 /// </summary> Active = 10, /// <summary> /// バックグラウンド実行中 /// </summary> Background = 20, /// <summary> /// 停止中 /// </summary> Suspended = 30, } /// <summary> /// Windows Phone アプリケーション実行状態 /// </summary> public enum AppStatePhone { /// <summary> /// 未起動 /// </summary> NotRunning = 0, /// <summary> /// 実行中 /// </summary> Running = 10, /// <summary> /// 中断中 /// </summary> Dormant = 20, /// <summary> /// シャットダウン /// </summary> Tombstoned = 30, }
AppState~ はアプリの実行状態を表すただの enum です
Xamarin.Forms で受信した場合と、各プラットフォームで受信した場合を用意してます
/// <summary> /// アプリケーション実行状態の変更イベント /// </summary> public class AppStateChangedEvent : PubSubEvent<ChangedAppState> { } /// <summary> /// アプリケーション実行状態の変更イベント引数 /// </summary> public class ChangedAppState : EventArgs { /// <summary> /// 新しい実行状態 /// </summary> public AppState AppState { get; protected set; } /// <summary> /// コンストラクタ /// </summary> /// <param name="state">アプリケーション実行状態</param> public ChangedAppState(AppState state) { this.AppState = state; } } /// <summary> /// iOS アプリケーション実行状態の変更イベント引数 /// </summary> public class ChangedAppStateIOS : ChangedAppState { /// <summary> /// AppState の変換表 /// </summary> private static readonly Dictionary<AppStateIOS, AppState> ConvertToAppState = new Dictionary<AppStateIOS, AppState>() { {AppStateIOS.NotRunning, AppState.NotRunning}, {AppStateIOS.InActive, AppState.Running}, {AppStateIOS.Active, AppState.Running}, {AppStateIOS.Background, AppState.Suspended}, {AppStateIOS.Suspended, AppState.Terminated}, }; /// <summary> /// 新しい実行状態 /// </summary> public AppStateIOS NativeAppState { get; protected set; } /// <summary> /// コンストラクタ /// </summary> /// <param name="state">アプリケーション実行状態</param> public ChangedAppStateIOS(AppStateIOS state) : base(ConvertToAppState[state]) { this.NativeAppState = state; } } /// <summary> /// Windows Phone アプリケーション実行状態の変更イベント引数 /// </summary> public class ChangedAppStatePhone : ChangedAppState { /// <summary> /// AppState の変換表 /// </summary> private static readonly Dictionary<AppStatePhone, AppState> ConvertToAppState = new Dictionary<AppStatePhone, AppState>() { {AppStatePhone.NotRunning, AppState.NotRunning}, {AppStatePhone.Running, AppState.Running}, {AppStatePhone.Dormant, AppState.Suspended}, {AppStatePhone.Tombstoned, AppState.Terminated}, }; /// <summary> /// 新しい実行状態 /// </summary> public AppStatePhone NativeAppState { get; protected set; } /// <summary> /// コンストラクタ /// </summary> /// <param name="state">アプリケーション実行状態</param> public ChangedAppStatePhone(AppStatePhone state) : base(ConvertToAppState[state]) { this.NativeAppState = state; } }
こちらは EventAggregator でハンドリングするイベントクラスと Payload クラスです
これも Xamarin.Forms で受信した場合と、各プラットフォームで受信した場合を用意してます
使い方はかんたんで
var eventAggregator = new EventAggregator(); // イベントの受信登録 eventAggregator.GetEvent<AppStateChangedEvent>().Subscribe( (s, e) => { // 受信後の処理 ]); // イベントの発行 eventAggregator.GetEvent<AppStateChangedEvent>() .Publish(new ChangedAppStateIOS(AppState.Suspended));
こんな感じに受信と発行を記述するだけ!
EventAggregator がシングルトンとして仲介役をするので、多対多のイベントの送受信がかんたんに実装できます(あまり使いすぎると管理しきれなくなりますが;)
今回の場合はまず App クラスを次のように修正して UnityContainer に EventAggregator を登録します
/// <summary> /// 共通 アプリケーションクラス /// </summary> public class App { /// <summary> /// アプリケーションクラス参照 /// </summary> public static readonly App Current; /// <summary> /// アプリ内で管理するモジュールのコンテナ /// </summary> public static readonly UnityContainer Container = new UnityContainer(); /// <summary> /// コンストラクタ /// </summary> static App() { Current = new App(); } /// <summary> /// コンストラクタ /// </summary> private App() : base() { // Prism.Mvvm.Xamarin アセンブリを読み込み可能にするおまじない var autoWired = ViewModelLocator.AutoWireViewModelProperty.DefaultValue; } /// <summary> /// アプリケーション起動処理 /// </summary> public void OnLaunchApplication() { // EventAggregator の生成に UnityContainer を使います Container.RegisterType<IEventAggregator, EventAggregator>(new ContainerControlledLifetimeManager()); // ViewModel をインスタンス化するデフォルトメソッドを指定します ViewModelLocationProvider.SetDefaultViewModelFactory((type) => Container.Resolve(type)); // 画面遷移サービスの生成方法を注入します Container.RegisterType<INavigationService, NavigationService>(new ContainerControlledLifetimeManager()); // Page クラスの生成に UnityContainer を使います NavigationService.SetPageFactiory(type => Container.Resolve(type)); NavigationService.SetRootPage(new NavigationPage(new FirstPage())); Container.RegisterType<SecondPageViewModel>(new ContainerControlledLifetimeManager()); } /// <summary> /// メイン画面を取得します /// </summary> /// <returns>メイン画面</returns> public static Page GetMainPage() { return NavigationService.RootPage; } }
その次は Suspended を検知するために NavigationService を改修
/// <summary> /// 画面遷移サービス /// </summary> public class NavigationService : INavigationService { #region Privates ~ 中略 ~ /// <summary> /// EventAggregator /// </summary> private static IEventAggregator eventAggregator; #endregion //Privates /// <summary> /// 画面遷移用ルート画面 /// </summary> public static NavigationPage RootPage { get; private set; } /// <summary> /// コンストラクタ /// </summary> [InjectionConstructor] public NavigationService(IEventAggregator eventAggregator) { RootPage = new NavigationPage(); NavigationService.eventAggregator = eventAggregator; NavigationService.eventAggregator.GetEvent<AppStateChangedEvent>().Subscribe(this.OnAppStateChanged); } ~ 中略 ~ /// <summary> /// アプリケーション実行状態変更イベントハンドラ /// </summary> /// <param name="state">アプリケーション実行状態</param> private void OnAppStateChanged(ChangedAppState state) { var vm = RootPage.CurrentPage.BindingContext as INavigationAware; if (vm == null) { return; } var history = NavigationStack.FirstOrDefault(); if (history == null) { return; } switch (state.AppState) { case AppState.Running: vm.OnNavigatedTo(history.Parameter, history.NavigationMode); break; case AppState.Suspended: vm.OnNavigatedFrom(true); break; case AppState.NotRunning: case AppState.Terminated: break; } } }
Suspended の検知が目的ですが Running になった時も ViewModel.OnNavigateTo を呼び出すようにしてみました
あとは各プラットフォームごとにアプリ実行状態変更時にイベントを発行するコードを記述
/// <summary> /// アプリケーション代理クラス /// </summary> [Register("AppDelegate")] public partial class AppDelegate : UIApplicationDelegate { /// <summary> /// ウィンドウ /// </summary> protected UIWindow window; /// <summary> /// 起動完了時の処理 /// </summary> /// <param name="app">アプリケーション</param> /// <param name="options">オプション指定</param> /// <returns></returns> public override bool FinishedLaunching(UIApplication app, NSDictionary options) { Forms.Init(); window = new UIWindow(UIScreen.MainScreen.Bounds); window.RootViewController = App.GetMainPage().CreateViewController(); window.MakeKeyAndVisible(); return true; } /// <summary> /// Active に移行した際の処理 /// </summary> /// <param name="application">アプリケーションクラス</param> public override void WillEnterForeground(UIApplication application) { App.Container.Resolve<IEventAggregator>().GetEvent<AppStateChangedEvent>() .Publish(new ChangedAppStateIOS(AppStateIOS.Active)); } /// <summary> /// Backgroud に移行した際の処理 /// </summary> /// <param name="application">アプリケーションクラス</param> public override void DidEnterBackground(UIApplication application) { App.Container.Resolve<IEventAggregator>().GetEvent<AppStateChangedEvent>() .Publish(new ChangedAppStateIOS(AppStateIOS.Background)); } }
iOS の場合 AppDelegate に記述
なぜか base.WillEnterForeground や base.DidEnterBackground を実行すると呼んじゃダメ例外が発生するので、基底クラスの同名メソッドは呼ばないようにしています
/// <summary> /// アプリケーションクラス /// </summary> public partial class App : Application { ~ 中略 ~ /// <summary> /// Running に移行した際の処理 /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void Application_Activated(object sender, ActivatedEventArgs e) { PrismXamarin.App.Container.Resolve<IEventAggregator>().GetEvent<AppStateChangedEvent>() .Publish(new ChangedAppStatePhone(AppStatePhone.Running)); } /// <summary> /// Dormant に移行した際の処理 /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void Application_Deactivated(object sender, DeactivatedEventArgs e) { PrismXamarin.App.Container.Resolve<IEventAggregator>().GetEvent<AppStateChangedEvent>() .Publish(new ChangedAppStatePhone(AppStatePhone.Dormant)); } }
Windows Phone の方はこんな感じ
試しに iOS で実行!・・・適当に画面遷移してホームボタンでバックグラウンド実行に遷移させると
無事に NavigationService に Suspended への遷移が通知されました!