Reactive Extensions の FromEventPattern
を使うと、イベントを IObservable
に変換できて、複数のイベントに時系列な関係を与えたり、他のストリーム処理とシームレスに扱えたりします。
Xamarin .iOS でも .Android でもこの機能を使うことができて大変便利ですが、Xamarin.iOS の場合 AOT による制限に気をつける必要があります。
以下は、なんの変哲もない、「ボタンを押したらタイトルを ”Clicked!” に変える」コードです。
MyButton.TouchUpInside += (s, e) => MyButton.SetTitle("Clicked!", UIControlState.Normal);
これを FromEventPattern を使うとこう書けます。
Observable.FromEventPattern(MyButton, "TouchUpInside")
.Subscribe(x => MyButton.SetTitle("Clicked!", UIControlState.Normal));
このコード、iOSシミュレータでは正常に動作しますが、 実機では、ビルドは通りますが動作しません。 実行時にこんなエラーがでます。
System.InvalidOperationException: Could not find event 'TouchUpInside' on object of type 'MonoTouch.UIKit.UIButton'.
TouchUpInside
が無いと言われます。
これは AOT により生成されたコードに、このイベントが含まれないのだと推測します。イベント名を文字列リテラルで指定しているので、そこまでの解析は期待できないですよね。
シミュレータで動作したのは、この場合は AOT でなく JIT で動作しているため。以下でも言及されています。
対象がiOSシミュレーターである場合と、iOSデバイスである場合とで、大きく異なる。iOSシミュレーターは、エミュレーターではなく、あくまでMac OS Xが動作しているx86 CPUの上で動作している仮想マシンであり、アプリケーションはJITによって動作する。iOSデバイスはARMであり、iOSデバイス用にビルドされたアプリケーションはAOTによってARMのCPU命令に変換されており、ARM上でしか動作しない。
Xamarin.iOS では実機で動作させないと安心ならないと言われる所以です。
さて、このケースでは、FromEventPattern の別なオーバーロードを使うことで解決です。
Observable.FromEventPattern(
h => MyButton.TouchUpInside+=h,
h => MyButton.TouchUpInside-=h)
.Subscribe(x => MyButton.SetTitle("Clicked!", UIControlState.Normal));
Xamarin.iOS の制限事項は以下に。
これまでこの制限に引っかかった事がなかったのですが、初めて引っかかりました。
メソッドを文字列リテラルで書いた時点で私の負けです、本当にありがとうございました。
いくつか追記
@espresso3389 さんも、同様のことを書いておられました。 [PreserveAttribute]
なるほど。
@nuits_jp さんも 調べてくれました。 現在のバージョン(Xamarin.iOS 9.8)でも同様です。また Xamarin.Forms では発生しないようですが、 これは Xamarin.Forms 内部で使われているから AOT に消されないというだけで、必ず消されない保証はありません。
Xamarin.Forms Behavior の公式ドキュメントに、下のようなコード例が紹介されているんですが、これも AOT で消される可能性を持っています、使うときは注意しましょう。