Xamarin.Forms では、左からスライドして出てくるメニューを持つ画面を MasterDetailPage で作成します。

一方、普通に画面遷移していく場合は ContentPage などを NavigationPage でラップしてあげます。

やりたいこと

何がしたいかというと、両者を組み合わせたいんです。こういうことってよくありませんかね?

起動画面で「新規ユーザー登録」があって、「ユーザー登録画面」を経て、メインの画面に遷移する、メイン画面にはスライドメニューがある、というパターン。これを Xamarin.Forms でやりたいのです。

問題

ところが、 NavigationPage で遷移していく画面の中に MasterDetailPage があると、 NavigationPage の方が勝ってしまい、ナビゲーションバーには「BACK」ボタンが表示されてしまいます。

これを消そうと、MasterDetailPage のコンストラクタで NavigationPage.SetHasBackButton(this, false) してみます。

その結果がこれ。

Android の方は望む結果になったけど、iOSの方はうーん…、BACKボタンは消えたけど、メニューを表示させるボタンが出ません。

しょうがないので、iOS の場合だけ、ナビゲーションバーの左ボタンをどうにかして追加してみます。

私が求めていたソリューション(CustomRenderer編)

Xamarin.Forms のお供、CustomRenderer です。

MasterDetailPage の iOS向けCustomRenderer を作って、ネイティブ側でナビゲーションバーをカスタマイズしてみます。

// CustomMasterDetailRenderer.cs
using System;
using MasterDetail.iOS;
using MonoTouch.UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(MasterDetailPage), typeof(CustomMasterDetailRenderer))]
namespace MasterDetail.iOS
{
    public class CustomMasterDetailRenderer : PhoneMasterDetailRenderer
    {
        public override void ViewWillAppear(bool animated)
        {
            base.ViewWillAppear(animated);

            var page = Element as MasterDetailPage;

            var navigationItem = this.NavigationController.TopViewController.NavigationItem;
            navigationItem.LeftBarButtonItems = new UIBarButtonItem[]
            {
                new UIBarButtonItem("MENU", UIBarButtonItemStyle.Plain, (_, __) => 
                { 
                    page.IsPresented = !page.IsPresented; 
                })
            };
        }
    }
}

「MENU」ってボタンを、ナビゲーションバーの左側に追加しています。
こんな CustomRenderer を iOS 側のプロジェクトに追加して実行してみます。

オーケーオーケー、これが私が求めていたソリューションです。

私が求めていたソリューション(Effects編)

が、CustomRenderer にはいくつか考えなければならないことがあります。

  • 上で作った CustomMasterDetailRenderer.csPhoneMasterDetailRenderer というクラスを継承しています。が、実はこれは iPhone 用で、実はタブレット(iPad)用に TabletMasterDetailRenderer というクラスもあります。これの CustomRenderer も用意しなければなりませんか?
  • CustomRenderer はベースとなる ViewRenderer を「継承」して作ります。そして C# は多重継承を許していません、この意味が分かるな?別の機能を拡張したいと思ったらCustomMasterDetailRendererから派生させるしかなくなります。

で、 Xamarin.Forms には、v2.1 から既存機能の拡張に Effects という選択肢が加わりました。

では、CustomMasterDetailRenderer.cs を Effects に変えてみましょう。

// CustomMasterDetailEffect.cs
using System;
using MasterDetail.iOS;
using MonoTouch.UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ResolutionGroupName("mycompany")]
[assembly: ExportEffect(typeof(CustomMasterDetailEffect), "CustomMasterDetailEffect")]
namespace MasterDetail.iOS
{
    public class CustomMasterDetailEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            var page = Element as MasterDetailPage;
            page.Appearing += Page_Appearing;
        }

        protected override void OnDetached()
        {
            var page = Element as MasterDetailPage;
            page.Appearing -= Page_Appearing;
        }

        void Page_Appearing(object sender, EventArgs e)
        {
            var vc = GetParentViewController();
            var page = Element as MasterDetailPage;

            var navigationItem = vc.NavigationController.TopViewController.NavigationItem;
            navigationItem.LeftBarButtonItems = new UIBarButtonItem[]
            {
                new UIBarButtonItem("MENU", UIBarButtonItemStyle.Plain, (_, __) => 
                { 
                    page.IsPresented = !page.IsPresented; 
                })
            };
        }

        UIViewController GetParentViewController()
        {
            UIResponder responder = this.Container;
            while ((responder = responder.NextResponder) != null)
            {
                if (responder is UIViewController)
                {
                    return (UIViewController)responder;
                }
            }
            return null;
        }
    }
}

Effect は、 ResolutionGroupNameExportEffect で定義した名称を使って、PCL側プロジェクトで Page に追加します。

// RootPage.cs
public class RootPage : MasterDetailPage
{
    public RootPage ()
    {
        NavigationPage.SetHasBackButton(this, false);
        // Effect を追加する
        Effects.Add(Effect.Resolve("mycompany.CustomMasterDetailEffect"));

        // 以下省略

こちらも、CustomRenderer と同じことができました。

が、ちょっと黒魔術っぽいの使ってます。

  • Effects.Container から取得できるのは UIView です。親の UIViewController を得るには、 GetParentViewController() でやってるような事をしなければなりません
  • CustomRenderer はそれ自体は ViewController だったので ViewWillAppear() など画面のライフサイクルコールバックを override することができました。が、Effects から ViewController のライフサイクルイベントをハンドリングできません。代わりに Xamarin.Forms 側の Page のライフサイクルから Appearing イベントで処理するようにしています。そのため、「MENU」ボタンが表示されるタイミングが若干遅れます。

CustomRenderer と Effects 、どちらを使えばいいの?

  • CustomRenderer はできる事は多いが、複数の CustomRenderer を適用することはできない
  • Effects はできる事は少ないが、複数の機能拡張を同時に適用できる

以上を考えると、

  1. まずあなたの行いたいことが Effects で実現できないか、試してみる
  2. Effects でできないレベルなら CustomRenderer を選択する

となるでしょう。

本件のネタは、 Effects ではかなりムリをして実現しているので、CustomRenderer の方が相応しいと思われます。
が、CustomRenderer はここぞという時にとっておきたい気もします。
このさじ加減は、作るアプリの規模・深度、汎用性、再利用性などによって変わってくるでしょう。Effects の方が汎用性・再利用性は高いですが、ネイティブのUIパーツをごっそり入れ替えるような深い事は、CustomRenderer でなければできません。

今回のプログラムは Github に上げてあります。(CustomMasterDetailRenderer.cs はコメントアウトしてあって、Effects の方を活かしてます。)

私は「Effects で頑張りたい派」かな。

Xamarin
Xamarin.Forms
iOS
C#

published

Ads

Read more!

amay077

Microsoft MVP(Xamarin). フルリモートワーカー. Geospatial Mobile app developer. Love C#.

amay077 amay077