"Titanium Advent Calendar 2011" 11日目担当のid:chris4403です。よろしくおねがいしまっす。
Titanium Mobile (以下、Titanium)とのお付き合いですが、昨年の9月頃から。Objective-C(以下、ObjC)に苦手意識を持っていた僕は、「JavaScriptでiPhoneアプリが作れる、しかも良い感じで」という謳い文句に飛びつきました。これまでにリリースしたのは以下の2つ。いずれもTitanium製です。
これ以外にも、リリースしていないけど試しに作ってみたアプリが手元にいくつかあります。
さて、本題。
Titaniumでアプリを作っていると、「ここであれしたいなー」「もっとこんなことしたいなー」と思っても、APIに自分がやりたいことを実現する機能がなくて「じゃあ諦めるか・・・」みたいな感じになることありますよね?(Titaniumあるある)
じゃあモジュールを作ったら?って感じになるんですが、そもそもObjCが面倒で/書けなくて、JavaScriptで作れるTitaniumを選択したわけで、ObjCで書かないといけないのはいやだ!と思う人は多いと思います。僕もそうでした。
でも、ObjCでカッコイイアプリをさらさらと作っている同僚のエンジニアを見ていると、僕もObjCで書けるようにならないとダメだ!と思うようになりました。ObjCが書けないから仕方なくJavaScriptで書いてるのと、ObjCで書けるけどあえてJavaScriptで書いているのは全然違いますからね。
ということで、今回は、ObjCでアプリを作ったことがない僕が、Titanium Mobileのモジュールをひとつ作ってみたいと思います。
さて、どんなモジュールがいいかなーと思ったんですが、iOS5がリリースされて、TwitterのアカウントがOSと密になり、組み込みのつぶやきシートが搭載されました。これを使えば、webviewやsafariでweb版のtwitterのintent apiを呼び出して、みたいな必要がなくなりとても楽ちんです。ユーザーがTwitterを使っていればログイン状態とか気にしなくていいですし、アプリの外にユーザも逃げませんしね。ということで、このつぶやきシートを呼び出すモジュールを作成してみようと思います。
ちなみに、同じようなモジュールはTitanium Mobileのmarketにすでにアップされてますし、Titanium自体のバージョンアップでこの辺もサポートされるでしょうから、短命のモジュールになると思っています。いいんです、今回はモジュールをひとつ作ってみるのが目的なので。
モジュールの作り方ですが、appcelerator公式のドキュメントは以下のurlにあります。ただ、内容が若干古い部分もあるので注意が必要です。
http://wiki.appcelerator.org/display/guides/iOS+Module+Development+Guide
では、ここに書かれているとおりに進めて行きましょう。
Step 0 . 環境設定
titaniumというコマンドが使えるように、シェルの設定ファイルにaliasを設定します。マニュアルではbashを使ってますが、僕はzshを使っているので ~/.zshrcに以下の一行を加えます。
alias titanium='/Library/Application\ Support/Titanium/mobilesdk/osx/1.7.3/titanium.py'
1.7.3の部分は自分のTitanium mobileのバージョンに合わせてください。
入力したら、ファイルを保存してsourceします。
source ~/.zshrc
これで、ためしにtitaniumコマンドを実行してみます。
~/% titanium Appcelerator Titanium Copyright (c) 2010-2011 by Appcelerator, Inc. commands: create - create a project run - run an existing project emulator - start the emulator (android) docgen - generate html docs for a module (android) fastdev - management for the Android fastdev server help - get help
こんな感じでコマンドが認識されたらOKです。
Step 1. モジュールを作る
以下のコマンドを実行すると雛形が出来上がります。
titanium create --platform=iphone --type=module --dir=~/tmp/ --name=simpletweet --id=com.chrisryu.simpletweet
dirパラメータは作成するディレクトリを、nameはモジュールの名前を、idは名前空間を指定します。
実行すると tmpディレクトリ以下にnameで指定した名前のディレクトリが作成されています。
~/% cd tmp/simpletweet ~/tmp/simpletweet/% ls -la total 88 drwxr-xr-x 18 chris staff 612 12 7 23:14 . drwxr-xr-x 6 chris staff 204 12 7 23:14 .. -rw-r--r-- 1 chris staff 20 12 7 23:14 .gitignore drwxr-xr-x 7 chris staff 238 12 7 23:14 Classes -rw-r--r-- 1 chris staff 62 12 7 23:14 ComChrisryuSimpletweet_Prefix.pch -rw-r--r-- 1 chris staff 78 12 7 23:14 LICENSE -rw-r--r-- 1 chris staff 4925 12 7 23:14 README drwxr-xr-x 3 chris staff 102 12 7 23:14 assets -rwxr-xr-x 1 chris staff 5855 12 7 23:14 build.py drwxr-xr-x 3 chris staff 102 12 7 23:14 documentation drwxr-xr-x 3 chris staff 102 12 7 23:14 example drwxr-xr-x 7 chris staff 238 12 7 23:14 hooks -rw-r--r-- 1 chris staff 396 12 7 23:14 manifest -rw-r--r-- 1 chris staff 777 12 7 23:14 module.xcconfig drwxr-xr-x 3 chris staff 102 12 7 23:14 platform drwxr-xr-x 3 chris staff 102 12 7 23:14 simpletweet.xcodeproj -rw-r--r-- 1 chris staff 388 12 7 23:14 timodule.xml -rw-r--r-- 1 chris staff 478 12 7 23:14 titanium.xcconfig
なんかいっぱいできてますね。
まずは、そのままbuildしてテストしてみましょう。以下のbuild.pyを実行します。
./build.py
でもって以下のコマンドを入力すると、ログが流れてiPhoneシミュレータが起動します。
titanium run
Hello Worldと表示されたアプリが起動しました。
このコマンドで実行されているのは、example以下のapp.js。中身を見てみましょう。
// This is a test harness for your module // You should do something interesting in this harness // to test out the module and to provide instructions // to users on how to use it by example. // open a single window var window = Ti.UI.createWindow({ backgroundColor:'white'}); var label = Ti.UI.createLabel();window.add(label); window.open(); // TODO: write your module tests here var simpletweet = require('com.chrisryu.simpletweet'); Ti.API.info("module is => " + simpletweet); label.text = simpletweet.example(); Ti.API.info("module exampleProp is => " + simpletweet.exampleProp); simpletweet.exampleProp = "This is a test value"; if (Ti.Platform.name == "android") { var proxy = simpletweet.createExample({message: "Creating an example Proxy"}); proxy.printMessage("Hello world!"); }
ここは普通のTitaniumのコードですね。モジュールをrequireして、exampleメソッドを呼び出しています。
では以下のコマンドでXcodeのプロジェクトを起動しましょう。
~/tmp/simpletweet/% open simpletweet.xcodeproj
起動したら、以下の2つのファイルを見てみます。
- ComChrisryuSimpletweetModule.h
/** * Your Copyright Here * * Appcelerator Titanium is Copyright (c) 2009-2010 by Appcelerator, Inc. * and licensed under the Apache Public License (version 2) */ #import "TiModule.h" @interface ComChrisryuSimpletweetModule : TiModule { } @end
- ComChrisryuSimpletweetModule.m
/** * Your Copyright Here * * Appcelerator Titanium is Copyright (c) 2009-2010 by Appcelerator, Inc. * and licensed under the Apache Public License (version 2) */ #import "ComChrisryuSimpletweetModule.h" #import "TiBase.h" #import "TiHost.h" #import "TiUtils.h" @implementation ComChrisryuSimpletweetModule #pragma mark Internal // this is generated for your module, please do not change it -(id)moduleGUID { return @"522a4ad0-d0eb-4c84-b433-8937541b318f"; } // this is generated for your module, please do not change it -(NSString*)moduleId { return @"com.chrisryu.simpletweet"; } #pragma mark Lifecycle -(void)startup { // this method is called when the module is first loaded // you *must* call the superclass [super startup]; NSLog(@"[INFO] %@ loaded",self); } -(void)shutdown:(id)sender { // this method is called when the module is being unloaded // typically this is during shutdown. make sure you don't do too // much processing here or the app will be quit forceably // you *must* call the superclass [super shutdown:sender]; } #pragma mark Cleanup -(void)dealloc { // release any resources that have been retained by the module [super dealloc]; } #pragma mark Internal Memory Management -(void)didReceiveMemoryWarning:(NSNotification*)notification { // optionally release any resources that can be dynamically // reloaded once memory is available - such as caches [super didReceiveMemoryWarning:notification]; } #pragma mark Listener Notifications -(void)_listenerAdded:(NSString *)type count:(int)count { if (count == 1 && [type isEqualToString:@"my_event"]) { // the first (of potentially many) listener is being added // for event named 'my_event' } } -(void)_listenerRemoved:(NSString *)type count:(int)count { if (count == 0 && [type isEqualToString:@"my_event"]) { // the last listener called for event named 'my_event' has // been removed, we can optionally clean up any resources // since no body is listening at this point for that event } } #pragma Public APIs -(id)example:(id)args { // example method return @"hello world"; } -(id)exampleProp { // example property getter return @"hello world"; } -(void)exampleProp:(id)value { // example property setter } @end
色々書いてありますが、pragma Public APIsとコメントしてある下に色々とメソッドなりを書いていけば良さそうです。
先ほどテストのファイルで呼び出したexampleメソッドもここに書かれていますね。NSStringをreturnするだけの簡単なメソッドでした。
Step 2. モジュールを書く
さて、ではさきほどのテスト用のexample/app.jsを以下のように書き換えてみます。
// open a single window var window = Ti.UI.createWindow({ backgroundColor:'white' }); window.open(); // TODO: write your module tests here var simpletweet = require('com.chrisryu.simpletweet'); Ti.API.info("module is => " + simpletweet); simpletweet.showTweetWindow('hoge');
テスト用のメソッドを消して、新たにshowTweetWindowというメソッドを書いています。
この状態で、titanium runしてみましょう。
[INFO] module is => [object ComChrisryuSimpletweetModule] [ERROR] Script Error = Result of expression 'simpletweet.showTweetWindow' [undefined] is not a function. at app.js (line 11). [DEBUG] application booted in 91.612041 ms
当たり前ですが、エラーになりました。showTweetWindowが定義されていないわけですね。
では、Xcodeに戻って、さっきみた2つのファイルを変更していきます。
ここで参考にしたのは、Apple DeveloperにアップされていたTwitterのサンプルファイルです。
まずはXcodeのプロジェクトの設定から以下のライブラリを利用可能にします。
- UIKit
- Accounts
プロジェクト名 → target → Build Phases → Link Binary With Librariesの「+」をクリックすると、ライブラリを選択するポップアップが表示されます。
ここで先ほどの3つのライブラリを追加していきます。
でもって、ComChrisryuSimpletweetModule.hで、importします。
#import <UIKit/UIKit.h> #import <Twitter/Twitter.h> #import <Accounts/Accounts.h>
続いてComChrisryuSimpletweetModule.mの方へ。import宣言のところで、TiApp.hをimportします。これはTwitter Frameworkを利用して取得するTWTweetComposeViewControllerを開くために利用しているのですが、この辺りはもっといい方法があるのなら知りたいです。わからない部分なTitanium Mobile本体のコードを参考にしてみました。
#import "TiApp.h"
#pragma Public APIs以下に書かれていたサンプルコードを削除して、代わりに以下のメソッドを追加します。
#pragma Public APIs -(void)showTweetWindow:(id) args { ENSURE_UI_THREAD(showTweetWindow, args); ENSURE_ARG_COUNT(args, 1); id value = [args objectAtIndex:0]; ENSURE_STRING(value); TiApp * tiApp = [TiApp app]; TWTweetComposeViewController *tweetViewController = [[TWTweetComposeViewController alloc] init]; [tweetViewController setInitialText:value]; [tweetViewController setCompletionHandler:^(TWTweetComposeViewControllerResult result) { // TODO check result switch (result) { case TWTweetComposeViewControllerResultCancelled: break; case TWTweetComposeViewControllerResultDone: break; default: break; } [tiApp hideModalController:tweetViewController animated:YES]; }]; [tiApp showModalController:tweetViewController animated:YES]; }
ポイントがいくつかあります。
- UIKitを使うときは、 マクロENSURE_UI_THREADで実行しないと失敗します。
- 引数はidでうけて、配列から取り出して型変換します。
ここまでで、保存して ./build.py 、titanium runを実行します。
無事に起動、といきたいところですが、真っ赤なエラー画面が。
以下のようなエラーが発生しています。
[ERROR] Error: Traceback (most recent call last): [DEBUG] File "/Library/Application Support/Titanium/mobilesdk/osx/1.7.3/iphone/builder.py", line 1148, in main [DEBUG] execute_xcode("iphonesimulator%s" % link_version,["GCC_PREPROCESSOR_DEFINITIONS=__LOG__ID__=%s DEPLOYTYPE=development TI_DEVELOPMENT=1 DEBUG=1 TI_VERSION=%s %s" % (log_id,sdk_version,debugstr)],False) [DEBUG] File "/Library/Application Support/Titanium/mobilesdk/osx/1.7.3/iphone/builder.py", line 1066, in execute_xcode [DEBUG] output = run.run(args,False,False,o) [DEBUG] File "/Library/Application Support/Titanium/mobilesdk/osx/1.7.3/iphone/run.py", line 39, in run [DEBUG] sys.exit(rc) [DEBUG] SystemExit: 65 [ERROR] Build Failed. See: /var/folders/2j/2jh+nTn5EAGCQp+ZFWvMwk+++TM/-Tmp-/mGn0GOAti/simpletweet/build/iphone/build/build.log
ここで少しはまったんですが、Xcodeでライブラリを追加したりしたときはmodule.xcconfigにそのライブラリを利用する記述を追加する必要があるんですね。module.xcconfigを開いて、以下の行を追加します。
// OTHER_LDFLAGS=$(inherited) -framework Foo OTHER_LDFLAGS=$(inherited) -framework UIKit -framework Accounts -framework Twitter
さあ、これで./build.py、titanium runを実行してみましょう。
Twitterアカウントが設定されていない端末だと、まずはアカウントを設定することを促すポップアップが表示されます。
画面の支持に従ってアカウントを設定します。
でもって、もう一度 titanium runで起動します。
無事につぶやきシートを表示することができました!やった!
Step 3. パッケージ化する
manifestというファイルを開きます。
# # this is your module manifest and used by Titanium # during compilation, packaging, distribution, etc. # version: 0.1 description: My module author: Your Name license: Specify your license copyright: Copyright (c) 2011 by Your Company # these should not be edited name: simpletweet moduleid: com.chrisryu.simpletweet guid: 522a4ad0-d0eb-4c84-b433-8937541b318f platform: iphone minsdk: 1.7.3
ここにバージョン情報や作者名、ライセンス、最小対象SDKバージョンなどを記入します。
# this is your module manifest and used by Titanium # during compilation, packaging, distribution, etc. # version: 0.1 description: simple tweet module author: chris4403 license: MIT License copyright: Copyright (c) 2011 by chris4403 # these should not be edited name: simpletweet moduleid: com.chrisryu.simpletweet guid: 522a4ad0-d0eb-4c84-b433-8937541b318f platform: iphone minsdk: 1.7.3
これでもう一度 ./build.pyすればOK。
Step 4.アプリに組み込む
buildすると、ディレクトリにzipファイルが生成されています。
~/tmp/simpletweet/% ls *.zip com.chrisryu.simpletweet-iphone-0.1.zip
このzipファイルを、モジュールを利用したいプロジェクトにコピーします。
tiapp.xmlのmodulesを以下のように変更します。
<modules> <module platform="iphone" version="0.1">com.chrisryu.simpletweet</module> </modules>
これで、アプリケーションをbuildしなおせばOK、…なんですが、「Couldn't find module : hogehoge」みたいなエラーメッセージが出て、モジュールをうまく認識してくれないことがあります。
Titanium Studioを利用しているときは、projectのcleanを実行、利用してない場合はアプリケーションのbuild/iphone以下のファイルを全て消して再度buildしなおせば認識してくれるはず。
これで、アプリケーションの中でモジュールが利用できるようになりました。
はてなカウンティングのTwitter投稿ボタンを押したときに呼び出すように組み込んでみたのが、下の画像です。
まとめ
今回作成したモジュールは以下にアップしています。
https://github.com/chris4403/TiSimpleTweet
食わず嫌いのObjCに挑戦してみようと思い立って、モジュール作りにトライして見ましたが、ObjCの勉強のとっつきにはよさそうです。
モジュール化することで、プログラムで実現したい機能がかなり絞られますし、そのため、他のモジュールのコードやサンプルコードを参照するときのポイントがクリアになって効率が良さそうでした。
ObjCに挑戦してみたいけど腰が引けている方は、一度モジュール作成から門戸を叩いてみるのもひとつの方法かもしれません。
今回作ったモジュールは、Twitter.frameworkにある画像やURLを追加するメソッドにも対応して、少し拡張してきたいと思います。
※ちなみに、このエントリを書いているときにTitanium Studioがversion upして、Titanium Mobile Moduleをprojectとして作ることができるようになりました。モジュールの作り方も、ここにまとめたのとは違う方法で作ることになりそうですね(せっかく書いたけど)。
次は@atsusyさんですね!よろしくお願いします!