iOSのカスタムPasteboardにアクセスするためのネイティブモジュール TiCustomPasteboardを作ってみた。

Titanium歴2年半以上ながら永遠の初心者を標榜しておりますdonayamaです。こんばんわ。
初心者たる理由のひとつとしてモジュールのひとつも作ったことがないという点がありました*1が、ついに非常に単純なものではありますが、モジュールを作ってしまったので、そのご報告かねがね記事にしてみようかと。

いきさつ

先日Titanium Newsでも紹介した自作アプリを開発するなかで、どうしても外部アプリ連携、具体的にはATOKPadとの連携機能を実現したいと思ったのです。
で、ジャストシステムのサイトを確認して、開発者用のドキュメントを読んでたら、カスタムPasteboardを介してテキストのやり取りをすると書いてあるじゃないですか。APIドキュメントを確認してもそんな機能はないですし、ObjCのソースを見てもUndocumentedな機能としても存在しないことが分かりました。

ならば選択肢は二つで、ストレートにモジュールを作るか、諦めるかのいずれかというわけで。
一旦は全文コピペを活用した連携ができることから、後者を選んだのですが、やはりせっかく夏の課題をすげかえてやっているのにチャレンジをしないというのは問題あるだろうということで、一念発起してモジュール作成をすることにしました。

開発する

要件は非常に単純で、指定した識別文字列のPasteboardに対して、文字列のI/Oを行うというもの。
カスタムPasteboardについてググった際に必要なコード例はだいたい分かっていたのですが、どのようにメソッド実装していけばいいのかというのが分からないまま、やおらTitaniumStudioの新規作成からTitanium Mobile Module Projectを選択します。

モジュール専用のプロジェクト作成ウィザード画面が表示されるので、ここにモジュール名を入れていきます。
プロジェクト名はふつうに大小文字混在のTiCustomPasteboardとしましたが、先達の教えもあったにも関わらずModuleIDを大小文字混在にしたためか、後々正しく動作しない罠にハマりました。
そこでIDは最初からjp.hsj.ticustompasteboardとしたものとして先に進めます(^^;
(次の画面ではあれこれモジュールに関する情報を放り込みます)


そうすると次のようなテンプレートが展開されます。

Finderなどでフォルダを開き、中の〜.xcodeprojファイルを利用してXcodeを起動します。
Classesフォルダ配下にある〜Module.mというObjCのソース(先ほどの例でいうとJpHsjTicustompasteboardModule.m)の後半に#pragma Public APIsと書かれた部分がありますので、そこにあるexampleメソッドをざっくり置換するところからスタートしてみました。

 >>>ここからかなり適当になってくるよ!w(間違っていたら教えてください…)<<<

必要なメソッドを作って行くわけですが、モジュールメソッドは最終的にJavaScriptへブリッジされるわけなので、引数や戻り値のやりとりに普通のObjCとはちょっと違う方法を用いるようです。

引数を一旦id型(中身は配列?)で受け取って、要素を抜き出しつつ、そのオブジェクトをTiUtilsというユーティリティクラスの各種メソッドでコンバートして使用するというアプローチ。
以下の例では0番目のindexの要素を文字列として取り出してNSStringの変数にセットしています。

-(void)doSomething: (id) args
{
   NSString* arg0 = [TiUtils stringValue: [args objectAtIndex:0]];
}

戻り値についてはNSStringなど一部のオブジェクトについては自動的な変換がかけられるようで、最終的にこんなソースになりました。(恥ずかしいけどそのままコピペ)

#pragma Public APIs

-(void)createPasteboard:(id)args
{
    // arg parse
    id arg = [args objectAtIndex:0];
	NSString *pasteboardName = [TiUtils stringValue:arg];
    
    // main
    UIPasteboard* pb = [UIPasteboard pasteboardWithName:pasteboardName create:YES];
    pb.persistent = YES;
}
-(void)removePasteboard:(id)args
{
    // arg parse
    id arg = [args objectAtIndex:0];
	NSString *pasteboardName = [TiUtils stringValue:arg];
    
    // main
    [UIPasteboard removePasteboardWithName:pasteboardName];
}

-(void)setString:(id)args
{
    // arg parse
    id arg0 = [args objectAtIndex:0];
    id arg1 = [args objectAtIndex:1];
    NSString *pasteboardName = [TiUtils stringValue:arg0];
    NSString *text           = [TiUtils stringValue:arg1];
    
    // main
    UIPasteboard* pb = [UIPasteboard pasteboardWithName:pasteboardName create:YES];
    pb.persistent = YES;
    [pb setString:text];
}

-(NSString*)getString:(id)args
{
    // arg parse
    id arg = [args objectAtIndex:0];
	NSString *pasteboardName = [TiUtils stringValue:arg];
    
    // main
    UIPasteboard* pb = [UIPasteboard pasteboardWithName:pasteboardName create:NO];
    return pb.string;
}

で、ビルドが通る事をXcode上で確認した後、再びTitanium Studioに戻ってきます。
パッケージ用のメニューがあるので、こちらからPackage iOS Moduleを選択します。

ウィザードでパッケージの作成先が選択できるので、全プロジェクトで共通して使うならSDKフォルダへ、個別のプロジェクト配下に入れるのならA Mobile appで対象プロジェクトを選択します。単にどこぞのフォルダに出力する場合はディレクトリ選択を選べばOKという簡単な仕掛けになっています。

使い方

先ほどのパッケージ先で個別プロジェクトを選択したところ、TiApps.xmlには自動的にモジュールが追加されます。便利!

あとは通常のモジュールと使い方は同じ。単純にPasteboardで文字列I/Oするだけなら以下のコードだけで実現できます。

var TiCustomPasteboard = require('jp.hsj.ticustompasteboard');
TiCustomPasteboard.setString('pasteboardName', 'text')
TiCustomPasteboard.getString('pasteboardName')

新たにPasteboardを作ったり、使用済みのPasteboardを破棄するには次のようなインターフェースを用意しています。

//  Pasteboard 
TiCustomPasteboard.createPasteboard('pasteboardName')
TiCustomPasteboard.removePasteboard('pasteboardName')
補足:他アプリとの連携事例

実際にどのようにATOKPadとの連携をしているかという点ですが、詳細に書くのは下記URLの記載内容に違反するような気がするので、ほわっと。
http://www.justsystems.com/jp/products/atokpad_iphone/developer.html

button.addEventListener('click', function(e){
    TiCustomPasteboard.setString('ATOK PadのPasteboard識別文字列', textarea.value);
    Ti.Platform.openURL('ATOKPadのカスタムURLスキーマとか');
});

カスタムURLスキーマのおしりのほうにコールバックされるアプリ本体のカスタムURLスキーマ情報などを付けておきます。

で、コールバックされるときにはアプリのresumeイベントが発生するのでそれで受け止めてやるイメージです。

Ti.App.addEventListener('resume', function(e) {
    setTimeout(function() {
        textarea.value = TiCustomPasteboard.getString(''ATOK PadのPasteboard識別文字列'');
    }, 200);
});

最後に

手習いで作ったためにObjCの実装も正直微妙ですし、機能もすごく不足しているのですが、こと最終目的であったATOKPadとの連携が実現できたので、個人的には満足しています。
ただ、名前が名前のモジュールなのでString以外の要素(Colorとか諸々)のやりとりを行えるように、いつかはしないとダメでしょうね…(^^;
実装も機能もチープなモジュールですが、みなさまのお役に立てれば幸いです。

https://github.com/donayama/TiCustomPasteboard

*1:もちろんビルドするぐらいは普通にやっていましたが…