MvvmCrossのiOS UniversalアプリでiPadで画面分割を作る

Xamarin Advent Calendar 2014 の 15 日目です。

4 日目には MvvmCross のプラグインの作り方を紹介しましたが、今回は MvvmCross の View 表示まわりに割り込みをして高度な表示機能を追加する IMvxViewPresenter の使い方をご紹介します。

MvvmCrossのView表示の仕組み

ところで、MvvmCross では ViewModel で ShowViewModel を実行するとサクッと View が表示されますが、実際は下で以下のような動きをしています。

  • MvxViewModelRequest が作られて、IMvxViewDispatcher.ShowViewModel が呼ばれる。
  • IMvxViewPresenter.Show にリクエストが転送される
  • IMvxViewPresenter の中で対応する View が生成されて表示される

IMvxViewPresenter には ViewModel から View を表示する以外にも、表示の変更要求メッセージである PresentationHint を受け取って処理することができます。MvvmCross では ViewPresenter を実装してカスタマイズすることで高度な画面表示の要求に応えることができます。

ViewPresenter 実装例

今回は以下のような、iPhone アプリをベースに考えます。メニュー(FirstView)があり、支店の一覧(BranchListView)があって、支店の詳細(BranchView)を表示しています。

スクリーンショット 2014-12-14 3.24.20.png

今回、このアプリの iPad 版を作ることになり、メニューはそのまま出すけど、支店の一覧と詳細は画面分割で 1 つの画面に表示という要求になっています。

スクリーンショット 2014-12-14 3.26.42.png

本来であれば UISplitViewController の出番なのですが、MvvmCross はデフォルトでルートが UINavigationController であり、SplitViewController が NavigationController の中に入るのは iOS プログラマとしてはきな臭さ MAX なので絶対やめた方がいいという考えに至ります。

そこで、iPad 用には画面分割するコンテナ ViewController を用意し、その中に ViewController を ChildViewController として追加する方法で対応することにします。

今回は以下の手順で対応していきます。

  • コンテナとなる ViewController とその中に入る ViewController の親子関係を定義するインターフェイスの作成
  • 親となる ViewController の作成
  • 子となる ViewController の設定
  • MvxViewPresenter の作成
  • MvxViewPresenter を MvvmCross に登録

インターフェイスの定義

まず、親子関係を定義するためのインターフェイスを用意します。

まず、子の ViewController には IPadContainerChild というインターフェイスを用意します。親となる ViewController の型を返します。

using System;

namespace SplitContainerSample.Touch.Views
{
    public interface IPadContainerChild
    {
        Type ContainerType { get; }
    }
}

親となる ViewController には IPadContainer というインターフェイスを用意します。

using System;
using Cirrious.MvvmCross.Views;

namespace SplitContainerSample.Touch.Views
{
    public interface IPadContainer
    {
        bool ShowView(IMvxView view);
    }
}

親となる ViewController の作成

支店詳細と支店一覧を格納する ViewController を作ります。BranchPadContainerなどとします。

スクリーンショット 2014-12-14 2.46.36.png

Xcode で画面分割された View を作ります。xib ファイルで View を 2 枚並べて、Autolayout でいい感じに分割されるようにし、左側の支店リストに当たる部分を BranchListViewContainer、右側の詳細に当たる部分を BranchViewContainer を名前をつけてアウトレット接続しています。(下の図はわかりやすいように色をつけています)

スクリーンショット 2014-12-14 2.48.44.png

using System;
using System.Drawing;

using MonoTouch.Foundation;
using MonoTouch.UIKit;
using Cirrious.MvvmCross.Views;

namespace SplitContainerSample.Touch.Views
{
    public partial class BranchPadContainer : UIViewController, IPadContainer
    {
        public BranchPadContainer() : base("BranchPadContainer", null)
        {
        }

        protected override void Dispose(bool disposing)
        {
            BranchListView = null;
            BranchView = null;
            base.Dispose(disposing);
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
        }

        #region ChildView Controller

        public BranchListView BranchListView
        {
            get { return _branchListView; }
            set
            {
                SwitchChildViewController(ref _branchListView, value, BranchListViewContainer);
            }
        }
        BranchListView _branchListView;

        public BranchView BranchView
        {
            get { return _branchView; }
            set
            {
                SwitchChildViewController(ref _branchView, value, BranchViewContainer);
            }
        }
        BranchView _branchView;

        /// <summary>
        /// 子 ViewController を切り替える
        /// </summary>
        /// <typeparam name="T">対象の子ViewControllerの型</typeparam>
        /// <param name="assignViewController">ViewController を格納している変数</param>
        /// <param name="newViewController">新しい ViewController</param>
        /// <param name="containerView">ViewController を表示する View</param>
        private void SwitchChildViewController<T>(ref T assignViewController, UIViewController newViewController, UIView containerView)
            where T : UIViewController
        {
            if (assignViewController != null)
            {
                assignViewController.WillMoveToParentViewController(null);
                assignViewController.View.RemoveFromSuperview();
                assignViewController.RemoveFromParentViewController();
                assignViewController.Dispose();
            }
            assignViewController = newViewController as T;
            if (assignViewController != null) {
                AddChildViewController(assignViewController);
                assignViewController.View.Frame = BranchListViewContainer.Bounds;
                containerView.AddSubview(assignViewController.View);
                assignViewController.DidMoveToParentViewController(this);
            }
            return assignViewController;
        }
          
        #endregion

        #region IPadContainer implementation

        public bool ShowView(IMvxView view)
        {
            if (view is BranchListView)
            {
                BranchListView = view as BranchListView;
                return true;
            }
            if (view is BranchView)
            {
                BranchView = view as BranchView;
                return true;
            }
            return false;
        }

        #endregion
    }
}

ここでのポイントは、子となる ViewController の追加や取り外しにはお作法があることです。ここでは SwitchChildViewController でまるっとまとめていますが、アニメーションなども考慮してこういう形になっています。詳しい内容は Objective-C の記事を childViewController などと調べてみてください。

また、IPadContainer の実装ですが、自分に格納できる View の場合は画面に追加して true を返し、そうでなければ false を返すようにしています。

子となる ViewController の設定

子となる ViewController では IPadViewContainerChild を実装して、親となる ViewController を返します。

namespace SplitContainerSample.Touch.Views
{
    public class BranchListView : MvxTableViewController, IPadContainerChild
    {

// ---- snip ---

        #region IPadContainerChild implementation

        public Type ContainerType
        {
            get { return typeof(BranchPadContainer); }
        }

        #endregion
    }
}

MvxViewPresenter の作成

今回は iPad だけの処理なので、iPad 専用の MvxViewPresenter のみ用意し、iPhone についてはデフォルトのものを使う方針で進めたいと思います。PadViewPresenter を作成します。

using Cirrious.MvvmCross.Touch.Views;
using Cirrious.MvvmCross.Touch.Views.Presenters;
using MonoTouch.UIKit;
using SplitContainerSample.Touch.Views;
using System;

namespace SplitContainerSample.Touch
{
    public class PadViewPresenter : MvxTouchViewPresenter
    {
        protected IPadContainer PadContainer
        {
            get
            {
                IPadContainer value = null;
                _padContainerHolder.TryGetTarget(out value);
                return value;
            }
            set
            {
                _padContainerHolder.SetTarget(value);
            }
        }
        WeakReference<IPadContainer> _padContainerHolder = new WeakReference<IPadContainer>(null);

        public PadViewPresenter(UIApplicationDelegate appDelegate, UIWindow window) : base(appDelegate, window)
        {
        }

        public override void Show(IMvxTouchView view)
        {
            if (view is IPadContainerChild)
            {
                var container = PadContainer;
                var child = view as IPadContainerChild;

                // 現在表示中のコンテナがあり、表示しようとしている View と一致している
                if ((container != null) && (container.GetType() == child.ContainerType))
                {
                    if (container.ShowView(view))
                    {
                        return;
                    }
                }

                // コンテナを作る
                container = child.ContainerType.GetConstructor(new Type[0]).Invoke(null) as IPadContainer;
                if (container == null)
                {
                    throw new InvalidOperationException();
                }
                var containerViewController = container as UIViewController;

                // View プロパティにアクセスしないと View が作成されないので先に呼んでおく
                containerViewController.View.ToString(); 

                if (container.ShowView(view))
                {
                    // 作成したコンテナで表示できれば NavigationController に作成したコンテナを追加
                    PadContainer = container;
                    this.MasterNavigationController.PushViewController(containerViewController, true);
                }
                else
                {
                    // ここまできて表示できなければそのまま表示してしまう
                    containerViewController.Dispose();
                    base.Show(view);
                }
            }
            else
            {
                base.Show(view);
            }
        }
    }
}

まず、表示中のコンテナがあり、表示しようとしている View が要求しているコンテナであればその中に表示しようとします。

表示中のコンテナがなかったり、表示中のコンテナが表示を拒否した場合は新規にコンテナを作ってその中に表示します。先に View プロパティを呼んでおかないと View が初期化されないので、先に呼ぶところまでを初期化の一環としています。(Objective-C では view を呼び出すだけで OK ですが、C# だと最適化で消えちゃうかもなあ、みたいなことを思ってとりあえず ToString を空打ちしてます。)

MvxViewPresenter を MvvmCross に登録

作成した ViewPresenter は MvvmCross によってデフォルトで作られている Setup クラスにある CreatePresenter メソッドをオーバーライドして返すことで使うことができます。

iPad 版のみ差し替えなので、UIDevice.CurrentDevice.UserInterfaceIdiom プロパティで iPad かどうかチェックしています。

using MonoTouch.UIKit;
using Cirrious.CrossCore.Platform;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Touch.Platform;

namespace SplitContainerSample.Touch
{
    public class Setup : MvxTouchSetup
    {
        public Setup(MvxApplicationDelegate applicationDelegate, UIWindow window)
            : base(applicationDelegate, window)
        {
        }

        protected override IMvxApplication CreateApp ()
        {
            return new Core.App();
        }

        protected override Cirrious.MvvmCross.Touch.Views.Presenters.IMvxTouchViewPresenter CreatePresenter()
        {
            if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad)
            {
                return new PadViewPresenter(ApplicationDelegate, Window);
            }
            return base.CreatePresenter();
        }
        
        protected override IMvxTrace CreateDebugTrace()
        {
            return new DebugTrace();
        }
    }
}

しあげ

さて、iPad で画面分割のサポートがあるアプリは、一番最初の項目が選択されている場合がほとんどです。

今回も画面表示時に支店一覧の最初の項目が選択されているようにしました。

public override void ViewWillAppear(bool animated)
{
    base.ViewWillAppear(animated);

    if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad)
    {
        var indexPath = MonoTouch.Foundation.NSIndexPath.FromRowSection(0, 0);
        TableView.SelectRow(indexPath, false, UITableViewScrollPosition.None);
        TableViewSource.RowSelected(TableView, indexPath);
    }
}

最後に

今回のプロジェクトはgithubにあげてあります。この記事中の内容はかなり端折っているので、こちらを実際に動かしてもらった方がわかるとおもいます。

https://github.com/iseebi/MvvmCross-SplitContainerSample

MvvmCrossのプラグインを作ってプラットフォーム依存コードを再利用する

Xamarin Advent Calendar 2014の4日目です。

MvvmCrossガチ勢としては書かざるを得ないと思っていたけど忙しくてかけていなかったMvvmCrossのプラグインの作り方をこの機会を借りて紹介したいと思います。

そもそもMvvmCrossプラグインとは

MvvmCrossプラグインは、MvvmCrossで作っているプロジェクトにカメラやバイブレーションといったプラットフォーム依存コードを再利用できる形で提供します。プラグインを組み込むと、IoCコンテナプラグインが提供しているクラスが登録されます。

公式でも多くのパッケージが公開されており、NuGetでMvvmCross Pluginで検索すると大量に出てきます。NuGetで公開されているものはプラグインを追加するとすぐに使い始めることができます。

また、プラグインは簡単に作ることができるので、自分たちでもよく使う機能はプラグイン化しておくことで再利用することができます。

プラグインの作り方

今回は簡易的なTextToSpeechの機能を作るという想定のもと、プラグインを作成してみようと思います。

続きを読む

iOS8においてカスタムキーボードの「フルアクセスを許可」をすると何が起こるのか

ATOK for iOS の設定手順で、キーボードを追加した後「フルアクセスを許可」を有効にするようにと案内されている。 この「フルアクセスを許可」というのは有効にした際に「開発元に送信することを許可します」とか書いてあって、ユーザーにとってはかなりきな臭い設定なわけだけど、実際にどういう設定なのかちょっと調べてみた。

続きを読む

フローティングタンクを試してきた

f:id:iseebi:20140819194726j:plain
最近仕事が忙しい時期が続き、それ以外にもいろいろな心配事を抱えていて精神的に結構辛い状況だった。何かリフレッシュできるものをと思ってた時に、以前Wikipediaをみてた時にふと見つけた、アイソレーションタンクというものを思い出した。
 
アイソレーションタンク、今はフローティングタンクと言うらしい。ざっくり言うと死海のごとくめっちゃ濃度の高い塩水をはった暗いカプセルのなかに浮くもので、浮いてるから重力も感じないし、水温は体温に合わせてるから水の感覚も感じないし、耳栓して水の中に耳が来るので音はほぼ聴こえないし、暗いから視覚もないし、という感じに感覚を断つことで、リラクゼーションや疲労回復などの効果があるものらしい。詳しくはWikipedia参照。 
実は見つけた時にすごく気になっていて、いつか試したいと思っていたので、ちょうど良いと思った。
 
 フローティングタンクが一般に使える場所は日本には数える程しかない。関西から最も近い、一般に使える施設は岡山のHIKARI CLINICという心療内科・精神科に併設されたCOCOONというというところになる。他のフローティングタンクおいてるところのWebサイトからは若干の怪しさを感じるところもあったけど、ここはそんな印象を受けなかったのも良いと思った。
 
お盆休みあわせで、岡山まで出向いて試してきた。
続きを読む

MvvmCross で Xamarin.Android の Fragment を使う方法と問題点について

Android 3.0 では、画面に表示される項目として、Activity の下に Fragment という要素を使うようになり、タブレット対応やタブの使用などでは必須レベルで使用するようになっています。

MvvmCross には、この機能に対して Fragging モジュールと、FullFragging モジュールという 2 種類の Fragment サポートがあります。 Fragging モジュールは Android 2.x でも使えるようにバックポートされた Support Library の Fragment を使用するもの、FullFragging モジュールは Android 3.0 以降に搭載される標準の Fragment を使用するものとなっています。

Fragging/FullFragging モジュールでは MvxFragment という View クラスが提供されますが、このクラスに関する使用方法がドキュメント上にありません。また、アプリが停止状態になる際の処理等が現状サポートされておらず、そのままの状態で使用するとメモリが少ない機種等で問題になってきます。

この記事では、MvxFragment クラスの使い方と、MvxFragment クラス上で停止機能が使えるように拡張する方法を紹介します。

続きを読む

Xamarin.iOS のネイティブバインディングを活用して既存の Objective-C 資産を流用する

Xamarin.iOS では、ネイティブバインディングを使用して Objective-C で書かれたライブラリを利用することができます。

つまり、既存のObjective-Cで書かれたアプリをXamarinに移行する際、特定の機能はネイティブライブラリにすることでObjective-Cのまま利用することができます。

ここでは、Objective-Cで書かれたアプリをUIViewControllerごとネイティブライブラリにし、Xamarin.iOSにネイティブバインディングで持ってくる方法をご紹介します。

続きを読む

Xamarin.Android で Java Integration ライブラリを作る

Xamarin では、広告・分析やUIなどの機能を提供するネイティブSDK(Java/NDK)も使用することができます。.NET用のライブラリだけでなく、ネイティブのライブラリが選択肢に入ることで、より多くの可能性を引き出すことができます。

ネイティブのライブラリを使用するには、バインディングライブラリというものを作ってラップする必要があります。この記事では Xamarin.AndroidJavaバインディングライブラリを作る方法をご紹介します。

続きを読む