2014年12月1日月曜日

OSX / iOS のデリゲートを全部取得する!

Delphi Advent Calendar 2014 12/01 の記事です。

2014/12/16 追記!


Twitter で @wyvern77 氏に "MethodNameAttribute" 属性を使えば Objective-C 本来の名前を指定できるぞ!と教えていただきました。
また、@owlsperspective 氏に、属性をまとめたページを教えてもらいました!
これによると、Macapi.ObjectiveC ユニットに MethodNameAttribute 属性が定義されていて、それをメソッドの属性にしてやればいいようです。
実は、まだ試していないのですが、とりあえず追記です!
もっといい方法を教えてください、と書いたかいがあった!
----- 追記ここまで ------------------------------



OSX / iOS 開発で必須になるのが Delegate。
もちろん FireMonkey だけでことが足りるなら必要ありませんが!

いわゆるデリゲートは、Delphi のメソッドポインタ(イベント)と同じで、オブジェクトへのポインタと関数へのポインタの両方を持つものとして、定義されてるみたい(wikipedia)です。

でも、OSX / iOS ではデリゲートは「委譲」(delegation)の方の意味合いが大きいです。
実際に、どういう時に使われるかというと OS が提供している機能の拡張手段だったりオブジェクト同士の通信だったりします。
そして、もちろんイベントにも使われる訳です。
たとえば、UIWebView のイベントハンドラとしての UIWebViewDelegate は
-webView:shouldStartLoadWithRequest:navigationType:
-webViewDidStartLoad:
-webViewDidFinishLoad:
-webView:didFailLoadWithError:
こんなメソッド(メッセージ)が定義されています。
で、こういった Delegate を Delphi で実装するのは、Interface の定義と TOCLocal から継承した2つのクラスが必要です。
FireMonkey では、こんな感じで定義されてます。
UIWebViewDelegate = interface(IObjectiveC)
['{25E7C20B-68A2-4011-9D7F-B97647BD48C0}']
procedure webView(webView: UIWebView; didFailLoadWithError: NSError);
cdecl; overload;
function webView(
webView: UIWebView;
shouldStartLoadWithRequest: NSURLRequest;
navigationType: UIWebViewNavigationType): Boolean; cdecl; overload;
procedure webViewDidFinishLoad(webView: UIWebView); cdecl;
procedure webViewDidStartLoad(webView: UIWebView); cdecl;
end;
 
 
TiOSWebViewDelegate = class (TOCLocal, UIWebViewDelegate)
public
procedure webView(webView: UIWebView; didFailLoadWithError: NSError);
overload; cdecl;
function webView(
webView: UIWebView;
shouldStartLoadWithRequest: NSURLRequest;
navigationType: UIWebViewNavigationType): Boolean; overload; cdecl;
procedure webViewDidFinishLoad(webView: UIWebView); cdecl;
procedure webViewDidStartLoad(webView: UIWebView); cdecl;
end;
で、あとは、下記のようにすればイベントが呼ばれます。
FDelegate := TiOSWebViewDelegate.Create;
FWebView.setDelegate(FDelegate.GetObjectID);
というか一般的なデリゲートの作り方と使い方は、むしろ Team J の「FireMonkey iOS - event delegateの使い方のサンプル」の記事の方が詳しいので、そちらをご覧ください。

ここからが本題。

実はですね、上記の方法では特定のデリゲートしか使えないのです。
ここで、問題になるのは、Object Pascal と Objective-C の文法・実行方法の違いです。
どういうことかというと、Objective-C では、メソッドとして見えるものはメッセージであり、メッセージは、そのパラメータを含めて1つのメッセージを構成しているということです。
つまり、引数の名前さえ違っていれば、別のメソッドとしてとらえられるということです。 たとえば、上記の UIWebViewDelegate でいえば

■Objective-C
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
 
-(void)webView:(UIWebView *)webView
didFailLoadWithError:(NSError *)error
 
こんな風に定義されているものが

■Object Pascal
procedure webView(
webView: UIWebView;
didFailLoadWithError: NSError); cdecl; overload;
 
function webView(
webView: UIWebView;
shouldStartLoadWithRequest: NSURLRequest;
navigationType: UIWebViewNavigationType): Boolean; cdecl; overload;
こうなるわけですが、ここで見てほしいのが "overload" です。
Objective-C では、メッセージはパラメータの引数の型ではなく、メッセージの名前とパラメータの名前で特定されます。
ですが、Object Pascal では、同じメソッド名の場合、引数の型が異なっている必要があります。
で、この違いがもたらす結果ですが…もうおわかりでしょうか。

たとえば、WebViewFrameDelegate は
-webView:didStartProvisionalLoadForFrame:
-webView:didFinishLoadForFrame:
-webView:didCommitLoadForFrame:
-webView:willCloseFrame:
-webView:didChangeLocationWithinPageForFrame:
こんな感じで、引き数名は全部違うものの型は全部 WebViewFrame です!!
Object Pascal で実装すると…
WebFrameLoadDelegate = interface(IObjectiveC)
procedure webView(
sender: WebView;
didStartProvisionalLoadForFrame: WebFrame); overload; cdecl;
procedure webView(
sender: WebView;
didFinishLoadForFrame: WebFrame); overload; cdecl;
procedure webView(
sender: WebView;
didCommitLoadForFrame: WebFrame); overload; cdecl;
procedure webView(
sender: WebView;
willCloseFrame: WebFrame); overload; cdecl;
procedure webView(
sender: WebView;
didChangeLocationWithinPageForFrame: WebFrame); overload; cdecl;
end
こうなります。しかし、引数の型が全部同じなので、コンパイラに怒られます!
これは困った…
FireMonkey では、どうやってるんだ!と思って調べたところ
UIPickerViewDelegate = interface(IObjectiveC)
// procedure pickerView(
// pickerView: UIPickerView;
// didSelectRow: NSInteger;
// inComponent: NSInteger); cdecl; overload;
// function pickerView(
// pickerView: UIPickerView;
// rowHeightForComponent: NSInteger): Single; cdecl; overload;
function pickerView(
pickerView: UIPickerView;
titleForRow: NSInteger;
forComponent: NSInteger): NSString; cdecl; overload;
// function pickerView(
// pickerView: UIPickerView;
// viewForRow: NSInteger;
// forComponent: NSInteger;
// reusingView: UIView): UIView; cdecl; overload;
end;
まさかのコメントアウト!!! oh...

で、ググったりしたものの解決策がなかったため、いろいろ考えました末に思いついたのが

type TBar = type TFoo;

構文!
この構文を使うと、別の型として同じ型を定義できます
そう!これを使えば!

type
WebFrame2 = type WebFrame;
WebFrame3 = type WebFrame;
WebFrame4 = type WebFrame;
WebFrame5 = type WebFrame;
 
WebFrameLoadDelegate = interface(IObjectiveC)
procedure webView(
sender: WebView;
didStartProvisionalLoadForFrame: WebFrame); overload; cdecl;
procedure webView(
sender: WebView;
didFinishLoadForFrame: WebFrame2); overload; cdecl;
procedure webView(
sender: WebView;
didCommitLoadForFrame: WebFrame3); overload; cdecl;
procedure webView(
sender: WebView;
willCloseFrame: WebFrame4); overload; cdecl;
procedure webView(
sender: WebView;
didChangeLocationWithinPageForFrame: WebFrame5); overload; cdecl;
end
こんな感じで定義できます!
もちろん、コンパイラに怒られません!
そして、ちゃんと OS からコールバックされます。

これで、全部のデリゲートを受け取れる!と思いきや、もう1つ問題がある事お気づきでしょうか?

それは、予約語の問題、です。

Object Pascal では様々な言葉が予約語になっていますが、その中で群を抜いて他の言語で使われるのが

type

です。
OSX / iOS でもパラメータ名として type が使われている事があります(もちろん他の予約語が使われている事も)。
たとえば、NSEvent には
@property(readonly) NSEventType type
というプロパティが定義されています。

ですが、これにはとても簡単に対処できます。
Object Pascal には、予約語を予約語として認識させないプレフィクスがあります。

それは、& です。

& を予約語の前につければ、コンパイラは予約語として扱いません。
先ほどの NSEvent の FireMonkey での実装は
NSEvent = interface(NSObject)
function &type: NSEventType; cdecl;
end;
こうなっています。
これで、予約語の問題も簡単に回避できました!

で、これらを駆使して Macapi.WebView.pas ができあがりました。
このソースの中の WevViewFrameDelegate の部分で type 構文が使われています。

これで全部のデリゲートを使用可能になりました。

ちなみに、ググっても見つからなかったのですが、これよりもよい方法をご存じでしたら教えてください!

0 件のコメント:

コメントを投稿