Experiments Never Fail

NavigationPage + MasterDetailPage の時に iOS の NavigationBar の左ボタンをカスタマイズする

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 にはいくつか考えなければならないことがあります。

で、 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 と同じことができました。

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

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

以上を考えると、

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

となるでしょう。

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

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

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

published at tags: Xamarin Xamarin.Forms iOS C#