【Titanium Advent Calendar 2011:十一日目】はじめてのTitanium Mobile Module作成 iPhone編

"Titanium Advent Calendar 2011" 11日目担当のid:chris4403です。よろしくおねがいしまっす。
Titanium Mobile (以下、Titanium)とのお付き合いですが、昨年の9月頃から。Objective-C(以下、ObjC)に苦手意識を持っていた僕は、「JavaScriptiPhoneアプリが作れる、しかも良い感じで」という謳い文句に飛びつきました。これまでにリリースしたのは以下の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のプロジェクトの設定から以下のライブラリを利用可能にします。


プロジェクト名 → 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さんですね!よろしくお願いします!