LoginSignup
11
10

More than 5 years have passed since last update.

Xamarin.iOSでUITableViewの罠 / Xamarinの進化に望む事

Posted at

余暇でもクロスプラットフォームで開発出来るラクチンソリューションを求めて、Xamarinに流れ着いたkochizufanと申します。
モバイル開発はObj-CでiOS開発を2年ほどやってましたが、c#、.NET及びAndroid開発歴は余暇で半年という超のつくド素人です。
よろしくお願いいたします。

XamarinでのUI開発の落とし穴?

言語の設計思想差に起因する陥穽にはまり込むと割と死ねます。

生産性の高いc#を使って、クロスプラットフォーム開発のできる便利なXamarinソリューションですが、無理にWrite Onceを目指さずネイティブAPIの上に薄いラッパを載せるという現実的な解を採用しているものの、やはりネイティブ基盤(Objective-CやJava)とc#の言語設計差の狭間に、時々落とし穴があるようで、これにはまり込むと割と死ねます。
これをバグと言うべきなのか、言語仕様差による仕方ないものと思うのかは議論が分かれるとは思いますが…。

その一例として、数ヶ月前私がどハマりしたものを。
サンプルコードは、
https://github.com/tilemapjp/Xamarin.iOS.2013AdventCalendar.TableView
に公開してあります。

Tabで二つのUITableViewを置いているだけのショボいサンプルですが、FirstViewControllerの方を見てください。

FirstViewController.cs

namespace TableViewTest
{
    public class FirstViewController : UITableViewController
    {
        ...

        public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
        {
            var message = String.Format ("{0} - {1}", indexPath.Section, indexPath.Row);

            if (EventInserted == 4) {
                message += "\nSome Event Inserted!";

                this.TableView.DecelerationStarted += (sender, e) => {
                    Console.WriteLine("DecelerationStarted");
                };

                ...
            }
            EventInserted++;

            var alert = new UIAlertView ("Selected!", message, null, "OK", null);
            alert.Show ();
        }
    }
}

何の変哲もない、UITableViewControllerのアプリで、セルをクリックしたらIndex値をポップアップするだけのようですが、5回目のクリックでUITableViewのScrollやDrag等関連のイベントハンドラが突っ込まれる模様。

実行してみましょう。

1回、2回、5回目までのクリックは、普通にポップアップが立ち上がりますが、5回目に「Some Event Inserted!」と出て以降の、6回目のクリックからはポップアップが立ち上がらなくなります。
代わりに、UITableViewを操作すると、ScrollやDrag関係のログがコンソールに流れるようになります。
MonoTouchのUITableViewControllerは、DelegateメソッドのRowSelectedと、イベントハンドラ指定が共存出来ないのです。

Objective-Cとc#でのイベント実装思想の違いを把握してないと気付けなさそう

数ヶ月前これに初めて出会った時は、イベントハンドラを登録したのが起因、というのに気付くのに酷く時間がかかりました。
が、イベントハンドラが起因というところからよく考えてみると、大元の原因が見えてきました。
どうも、Objective-Cのイベント通知メカニズムと、c#のイベント通知メカニズムの違いに原因がありそうです。

Objective-Cのイベント通知メカニズムは、与えられたDelegateオブジェクトのメソッドを叩く形なので、c#のイベントハンドラに翻訳しようと思うと、裏でダミーのDelegateオブジェクトを登録しておく必要があります。
UITableViewの親クラスはUIScrollViewですから、UIScrollViewのc#ラッパを作る際に、UIScrollViewDelegateインタフェースをイベントインタフェースに翻訳する事は自然です。
一方、UITableViewControllerのRowSelectedは、裏でUITableViewDelegateを乗っ取って実現していると思われますが、protocolの定義箇所は別でもUITableViewのDelegateとして登録できるのは一つだけですから、イベントハンドラを追加=>裏でダミーDelegateの追加処理が走り、UITableViewControllerのRowSelectedが効かなくなるみたいです。

これを解決して、UITableViewでセル選択とスクロールイベントを同時に取るには、イベントハンドラ登録を止めて、でもUITableViewControllerにはUIScrollViewDelegateのコールバックは定義されていませんから、多分サンプルのSecondViewControllerでやってるようなやり方しかないんじゃないかなと思います。

SecondViewController.cs

namespace TableViewTest
{
    public class SecondViewController : UIViewController
    {
        int EventInserted = 0;
        SecondViewDataSource _dataSource;
        SecondViewDelegate   _delegate;

        public UITableView TableView
        {
            get { 
                return (UITableView)this.View;
            }
        }

        ...

        public override void LoadView ()
        {
            base.LoadView ();

            var tableView = new UITableView ();
            this.View = tableView;

            _dataSource = new SecondViewDataSource (this);
            tableView.WeakDataSource = _dataSource;
            _delegate = new SecondViewDelegate (this);
            tableView.WeakDelegate = _delegate;
        }

        public int NumberOfSections (UITableView tableView)
        {
            return 100;
        }

        ...

        public void RowSelected (UITableView tableView, NSIndexPath indexPath)
        {
            var message = String.Format ("{0} - {1}", indexPath.Section, indexPath.Row);

            if (EventInserted == 14) {
                message += "\nSome Event Inserted!";

                this.TableView.DecelerationStarted += (sender, e) => {
                    Console.WriteLine("DecelerationStarted " + this.TableView.WeakDelegate.ToString());
                };

                ...
            }
            EventInserted++;

            var alert = new UIAlertView ("Selected!", message, null, "OK", null);
            alert.Show ();
        }
    }

    public class SecondViewDataSource : UITableViewDataSource
    {
        WeakReference viewController;
        SecondViewController ViewController {
            get { 
                return viewController == null ? null : (SecondViewController) viewController.Target;
            }

            set {
                viewController = value == null ? null : new WeakReference (value);
            }
        }

        public SecondViewDataSource (SecondViewController viewController) : base()
        {
            this.ViewController = viewController;
        }

        public override int NumberOfSections (UITableView tableView)
        {
            return ViewController.NumberOfSections (tableView);
        }

        ...
    }

    public class SecondViewDelegate : UITableViewDelegate
    {
        WeakReference viewController;
        SecondViewController ViewController {
            get { 
                return viewController == null ? null : (SecondViewController) viewController.Target;
            }

            set {
                viewController = value == null ? null : new WeakReference (value);
            }
        }

        public SecondViewDelegate (SecondViewController viewController) : base()
        {
            this.ViewController = viewController;
        }

        public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
        {
            ViewController.RowSelected (tableView, indexPath);
        }

        public override void DecelerationStarted (UIScrollView scrollView)
        {
            Console.WriteLine("DecelerationStarted " + ViewController.TableView.WeakDelegate.ToString());
        }

        ...
    }
}

この方法だと、セルクリックイベントとビュースクロールイベント等が一緒に扱えます。
15クリック目にわざとイベントハンドラ追加して、動かなくなる状態を作っていますが、コンソール出力しているTableViewのWeakDelegateのクラス名がイベントハンドラを追加した途端変わっているのも見て取れます。

必ずしもXamarinに起因する問題でもないけれど、気付き易いかにくいかに大きな差

とか書いてて今気付きましたけど、これって別にXamarin c#環境だけの問題じゃないかもしれませんね。
UITableViewControllerがTableViewのDelegateを裏で乗っ取ってるのはObjective-Cの方でそういう実装になっているのだろうし、Objective-CでもUITableViewControllerで、セル選択とスクロールイベント取得を同時にやろうとするとぶつかる問題っぽい…Objective-C使ってた頃は出会いませんでしたが。
その意味で、バグ、とは言えない問題だとは思います。
でも、Objective-Cだと明示的にDelegate登録しないといけないし、Delegate内にRowSelectedのコールバックもあるので気付き易そうですが、c#だとイベントプロパティが気軽にTableViewに生えてるし、Delegateは知らないうちに裏で乗っ取られてるし、なわけなので、なかなか気付けない可能性が高いと思います。

Xamarinの今後に望むこと

各プラットフォームの実装思想に関する知識は必須

半年Xamarin使ってきて感じた事ですが、上記に限らず、どハマりした事を解決しようと思うと、途端に元プラットフォームの知識が必要になる事が結構ありますね。
GithubのGoogle Maps for iOS SDKで、TileReceiverクラスのインタフェース定義が間違っててタイル地図のフックができなかった時もそうだし、
Androidでも、ActivityやFragmentのライフサイクルをよく知らないまま、シングルトンを多用したアプリ作ってて、後からライフサイクルの存在を知って涙を飲んだとか、そういうのが結構あるんですよね。
バインディング作成簡単と言っても、結局ちゃんとした間違いのない定義書こうと思えば、APIの正しい挙動を全部掴んでおく必要がありますし。

「c#で書ける!」アドバンテージと、使いたいけど使えないSDKがあるストレス間の綱引きは微妙

結局元プラットフォームの事をたくさん勉強しないといけない、勉強しても使いたいSDKのバインディングが出るか/作れるか保証がないストレスと、githubで見つけた野良実装をすぐに使える気安さを天秤にかけて考えた場合、
クロスプラットフォーム開発でもネイティブUIを、という路線の現実性には頷くものの、それをc#でやる必要があるか、というのには若干疑問を感じます。
どうせ各プラットフォームの特性を結構深くまで理解しないといけないのであれば、ネイティブUIはネイティブ開発環境で行う、という選択肢もあってもよいような気がします。

c#の快適さと共通化メリットはビジネスロジックでこそ発揮される

一方で、ビジネスロジック部分が共通化出来るのは、すばらしいアドバンテージだと感じます。
実際、一方のプラットフォーム向けに作っていたアプリを、ビジネスロジック部分をほぼそのまま持ってきて別プラットフォームの対応するUIに繋げてやればあっという間にマルチプラットフォーム化できて感動、というのはこの半年で何度も味わってきました。
でもそれと同じ位、気に入ったネイティブ実装がうまく持って来れないストレスというのも感じてきました。
Xamarinには、全てをc#で作るという選択肢の他に、Objective-C/Javaから呼び出せるビジネスロジックのライブラリを生成出来る選択肢もあれば嬉しいなあ、という期待を持っています。

そうなってくれればいいなあ…。

11
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
10