nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

Xamarin.FormsのNavigationPageで戻るボタンのイベントを取得する

またteratailネタなんですが、調べたり考えたりするのに面白いネタだと思い、週末少しいろいろやってみた結果を残しておこうかと思います。

https://teratail.com/questions/55706

質問者さんのポイントはいくつかあって

  1. iOSだとNavigationPageの戻るボタンのイベントが取得できない (AndroidだとNavigationPage#OnBackButtonPressedをオーバーライドすれば可能)
  2. 押されたタイミングでイベントをフックして進捗表示し、処理後に前画面へ戻りたい
  3. このためOnDisappearingなどでは処理できない

ということでした。
一応実現はできました。

f:id:nuitsjp:20161120185802g:plain

ただ、いろいろ調べたり考えた結果
「実装自体は可能だけど、やるべきではないケースが多そう」
という結論になりました。
以降は調査と考察の内容になります。

最初に結論

さて、上記の課題はぼちぼちメジャーな話のようで、結論から言うとリンク先の方法で対処は可能でした。

xamarinhelp.com

実際に実装してみたコードがこちらになります。

https://github.com/nuitsjp/XamarinSamples/tree/master/OnBackButtonPressed

考察

さて、これをやろうとした場合の最大の問題は
「スワイプによるナヴィゲーションバックを殺してやらなくてはならない」
という点にあります。
ユーザビリティに制約を設けてまで本当にやる必要があるのか?という点をよく検討する必要があるでしょう。

実装方法

とはいえ、せっかく調べたし、実現方法の解説は残しておきたいと思います。
今回の対応をするためには2つの対応が必要です。

  1. NavigationRendererをカスタマイズしてスワイプバックのジェスチャーを殺す
  2. PageRendererをカスタマイズして、戻るボタン押下時にVMに通知してあげる

の2つの対応になります。

以下がそのコードです。
iOSプロジェクトに作成してあげる必要があります。

[assembly: ExportRenderer(typeof(NavigationPage), typeof(CustomNavigationRenderer))]
namespace OnBackButtonPressed.iOS
{
    public class CustomNavigationRenderer : NavigationRenderer
    {
        public override void SetViewControllers(UIViewController[] controllers, bool animated)
        {
            base.SetViewControllers(controllers, animated);
            foreach (var controller in controllers)
            {
                // Disable swipe back
                ((UINavigationController)controller).InteractivePopGestureRecognizer.Enabled = false;
            }
        }
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            if (InteractivePopGestureRecognizer != null)
            {
                InteractivePopGestureRecognizer.Enabled = false;
            }
        }
    }
}

PageRendererをカスタマイズして、戻るボタン押下時にVMに通知してあげる

まずはVMで通知を受け取るためのインターフェースを作成します。
これはPCLのプロジェクトへ作成しましょう。

    public interface IConfirmGoBack
    {
        Task<bool> CanGoBackAsync();
    }

そして、PageRendererを継承して拡張します。
これはiOSプロジェクト側に作成します。

[assembly: ExportRenderer(typeof(Page), typeof(CustomPageRenderer))]
namespace OnBackButtonPressed.iOS
{
    public class CustomPageRenderer : PageRenderer
    {
        public override void ViewWillAppear(bool animated)
        {
            base.ViewWillAppear(animated);

            var page = Element as Page;
            var navigationPage = page.Parent as NavigationPage;
            var root = this.NavigationController.TopViewController;
            root.NavigationItem.SetLeftBarButtonItem(new UIBarButtonItem($"< Back", UIBarButtonItemStyle.Plain, async (sender, args) =>
            {
                var navPage = page.Parent as NavigationPage;
                var vm = page.BindingContext as IConfirmGoBack;

                if (vm != null)
                {
                    if (await vm.CanGoBackAsync())
                        navPage.PopAsync();
                }
                else
                    navPage.PopAsync();
            }), true);
        }
    }
}

これでOKです。
戻るボタンのイベントを受けたいVewModelでIConfirmGoBackインターフェースを実装してあげればイベントをフックできます。
こんな感じです。

    public class SecondPageViewModel : IConfirmGoBack, INotifyPropertyChanged
    {
        private bool _isProcessing;

        public bool IsProcessing
        {
            get { return _isProcessing; }
            set
            {
                _isProcessing = value;
                OnPropertyChanged();
            }
        }

        public async Task<bool> CanGoBackAsync()
        {
            IsProcessing = true;
            try
            {
                await Task.Delay(2000);
                return true;
            }
            finally
            {
                IsProcessing = false;
            }
        }
    }

falseを返してあげれば戻るを止めることも可能です。

という訳で、実現自体は可能でしたが、前述の通り、UIに制約ができてしまうことや、そもそも使いやすいのか?それ?疑惑もあるため、用法容量を守ってご利用ください。

という訳で、今日はここまで。
それではまた!