2009年5月31日日曜日

NSTimerは基本的にretainせずassignでよい

NSTimerを初めて使ってみたのでハマったところをメモしておきます。


■NSTimerはNSRunLoopにretainされる。NSTimerは引数targetで与えられたオブジェクトをretainする。
いちばんハマったのがこの挙動です。
AppleのNSTimerについての公式ドキュメント(http://www.devworld.apple.com/documentation/Cocoa/Conceptual/Timers/Articles/usingTimers.html#//apple_ref/doc/uid/20000807-CJBJCBDE)にもクラスリファレンスにもきちんとと明記されていたのですが・・・思いっきり見落としてました。

これらがいったいどんな問題を引き起こすか。
たとえば普通のクラスと同じ感覚でdealloc中にNSTimerのinvalidateを呼び出すコードを書くと、永遠にdeallocが呼び出されなくなってしまいます。
// ViewControllerがNSTimerを使っているとして・・・
- (void)viewDidLoad
{
timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:self // このselfはretainされる
selector:@selector(timerFired)
userInfo:nil
repeats:YES];
}
- (void)dealloc
{
[displayLabel release];
[timerLabel release];
// ここでinvalidateしてはいけない、永遠にdealloc自体が呼び出されなくなる
[timer invalidate];
[timer release];
}

なぜなら、
  • NSTimerがdeallocを呼び出すオブジェクトをretainしている、したがってNSTimerがreleaseされるまでdeallocが呼ばれない
  • NSTimerはNSRunLoopにretainされている、従ってNSTimerのinvalidateが実行されるまではNSTimerはreleaseされない
  • invalidateを呼び出しているのはこのdeallocの中以外にない。・・・詰みました。
こうならないようにするためには、dealloc以外の箇所から、適切にinvalidateメソッドを呼び出してやる必要があります。ということで、ViewControllerの中でNSTimerを使うときは、以下の2点に気をつければ良さそうです。
  1. NSTimerのオブジェクトは基本retainしない(自分でNSRunLoopにaddTimerとかしたい場合は別として)
  2. 通常deallocのタイミングでオブジェクトをreleaseするのと同じようにNSTimerをinvalidateしたい場合は、- (void)viewWillDisappear:(BOOL)animated を使う

- (void)viewWillDisappear:(BOOL)animated
{
if (timer)
{
// ここでタイマーをinvalidateする
// invalidateするとNSRunLoopがretainされていたこのタイマーをreleaseしてくれる
[timer invalidate];
timer = nil;
}
}

■userInfoはただのNSDictionary
そのまんまです。クラスリファレンスを見ても
The user info the new timer.

This parameter may be nil.
としか書いてなくて困ったのですが、本当にただのNSDictionaryです。適当に使ってくれってことでしょうか。


■次のタイマーイベントまでの間隔をリセットしたいときはsetFireDate:
たとえば何らかの理由でタイマーのFireイベントに登録していたセレクタを自分で呼び出しちゃって、次のタイマーイベントまでの間隔をリセットしたいときなどは以下のような具合にするとよいです。
if (timer)
{
// timerのsetFireDateに、次にタイマーイベントが発生する日時をセットする
// NSDateに、dateWithTimeIntervalSinceNowという便利なメソッドがあるのでこれを使う
[timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:interval]];
}

■っていうか
今このBlogを書くためにNSTimerでぐぐったらここに書いてあるようなことがいろいろ見つかってしまいました・・・><
先にちゃんと調べてから作らないと無駄ですね−。

2009年5月27日水曜日

Mac版ATOK + HHK Professional2で日本語入力をするときの操作方法まとめ

1年ほど愛用していたCENTURY社のWhite Knight (http://www.century.co.jp/products/ck-112cmw-psu.html) の調子が悪くなってしまい、ついにはキーを認識しなくなってしまったため、現在家のiMacにはHHK Professional2 黒 (http://www.pfu.fujitsu.com/hhkeyboard/hhkbpro2/) をつなげています。

ASCII配列自体にはある程度慣れてきたのですが、問題が日本語入力です。家のiMacではATOK for Mac 2008をMS-IME風スタイルで利用していますが、ATOKだろうがMS-IMEだろうがことえりだろうが、一般的には皆さん変換時に文節を矢印キーで動かしたり伸縮させたりしていることと思います。ところが、HHK Professional2にはまともな矢印キーがありません(あるにはありますが、これは矢印キーとして見なしてはいけないと思います・・・)。これでは日本語入力が出来ない!ということで、HHKらしくControlキーとアルファベットの組み合わせて日本語入力を華麗にこなすための操作方法をまとめてみました。

2009/06/01追記:以下のショートカットキーはMS-IME設定のときのものです。WindowsのMS-IME上でも全く同様に使えました!


■次の文節へ・前の文節へ
左手で行います。
Control + Dで次の文節へ
Control + Sで前の文節へ
Control + Aで先頭の文節へ
Control + Fで末尾の文節へ


■文節区切りの伸縮
右手で行います。
Control + Kで文節を短くする
Control + Lで文節を長くする


■文字の削除
変換中にControl + Hで単語削除
変換中にControl + Gで一文字削除


■ひらがな・カタカナ・英字などに一発変換
主に右手で行います。
Control + Uでひらがな
Control + Iでカタカナ
Control + Oで半角カタカナ
Control + Pで全角英字
Control + Tで半角英字


■これだけ覚えておけば大丈夫!
でもどうしても気に入らない場合は、「カスタマイザ」というとっても便利な機能があるので、すべて自分の好きなように変えてしまいましょう。


■しかし驚いたのは
ほとんどこれらの機能を使うことなくATOKが一発変換してくれたこと。実際、このBlogを書いている最中にも、Controlキーには2回程度しか触れていません。ATOKはマジに神。

2009年5月19日火曜日

UINavigationの片方のサイドに複数個のボタンを持たせたい



こんな感じでUINavigationBarの片方のサイドに複数個のボタンをおく方法を探してみました。UIToolBarと異なり、UINavigationBarではUINavigationItem.rightBarButtonItem, UINavigationItem.leftBarButtonItem, それからタイトル部分と、最大でも3個しかアイテムを配置することが出来ません。そのため、複数のボタンを一つの配置箇所にまとめて配置したい場合には、カスタムビューを作成する必要があります。
Appleの配布しているデモアプリケーションにもありますが、こういう場合にはUISegmentedControlのmomentaryプロパティをYESに指定して、ボタンみたいに利用するのがいちばん良いようです。
NSArray *items = [NSArray arrayWithObjects:@"Add", @"YourTurn", nil];
UISegmentedControl *segmentedControl = [[[UISegmentedControl alloc] initWithItems:items] autorelease];
segmentedControl.selectedSegmentIndex = UISegmentedControlNoSegment;
segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
segmentedControl.momentary = YES;
[segmentedControl addTarget:self action:@selector(segmentedControlClicked:) forControlEvents:UIControlEventValueChanged];
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithCustomView:segmentedControl] autorelease];

この方法でひとまずはUINavigationItemの右側に複数のボタンを作ることが出来ました。しかしまだまだ完璧ではなくて、現地点で分かっている限りでも二つの問題があります。
  1. ボタンの背景色が通常のボタンとは異なる色になる
  2. 個々のボタンひとつひとつを個別にDisableすることができない

Appigo Todoなんかは普通にUINavigationBarの右側に3つもまとめてボタンを綺麗に配置してあったりするのですが、一体全体どうやってるんでしょうね?・・・うーん。

2009年5月7日木曜日

Objective-CではvalueForKeyPath:で集計関数みたいなものが使えて凄く便利

Pythonだと、
list = [{'no':1, 'name':'akisute'}, {'no':2, 'name':'abesi'}, {'no':3, 'name':'hidebu'}]
maxNo = max(list, key=lambda x:x['no'])
こんな感じでリストに含まれるオブジェクトの最大値を簡単に取り出せたりするのですが、Objective-Cでもできないかと思い調べてみました。ですが、NSArray自体にはそのようなメソッドが用意されていません。ひょっとして出来ないのかと思っていたら、ちょっと面白い方法で集計関数のようなものが実装されていることがわかりました。

Objective-CにはKey-Value Codingという概念があって、それを使って実装されているようです。Key-Value Codingについては正直全然理解していないのでここでの解説は避けます。すみません。

NSArrayのvalueForKeyPathを使って以下のように問い合わせを行うと、先ほどのPythonの例と同様にNSArray中の最大値を持つオブジェクトを取得することが出来るようです。
// noとnameプロパティを持つPersonクラスがあると仮定して・・・
id akisute = [[Person alloc] initWithNo:1 name:@"akisute"];
id abesi= [[Person alloc] initWithNo:2 name:@"abesi"];
id hidebu= [[Person alloc] initWithNo:3 name:@"hidebu"];
NSArray *array = [NSArray arrayWithObjects:akisute, abesi, hidebu, nil];
NSNumber *maxNo = [array valueForKeyPath:@"@max.no"];
valueForKeyPathの引数に、@max.noというキーを渡すところがキモです。これで、NSArrayのインスタンスに含まれるオブジェクトのnoプロパティの最大値を求めることが出来ます。
同様にして、名前の最大値を求めたいときには、
NSString *maxName = [array valueForKeyPath:@"@max.name"];

とすれば取れます。また、一番長い名前の長さを求めたいときは、以下のように指定することができます。
NSNumber *maxCountOfName = [array valueForKeyPath:@"@max.name.count"];

@max以外にも、@avg, @sum, @count, @min, @unionOfObjects, @distinctUnionOfObjectsなどが用意されているみたいです。

詳しくは荻原さんのObjective-C 2.0本をご参照あれ。いや、この本は本当に買ってよかったです。Objective-Cのバイブルですね。

詳解 Objective-C 2.0
荻原 剛志
4797346809

Windows起動時にhal.dllが見つかりませんと言われた場合の対処法

久しぶりにBootCampでWindows XPを起動しようとしたら、hal.dllが見つかりませんといった具合のエラーメッセージが表示されてうんともすんとも言わなくなってしまいました。dllが破損したのかと思い調べてみたところ、このエラーメッセージはC:¥boot.iniファイルの設定が破損された場合に表示されるメッセージだと言うことが分かりました(さすがマイクロソフト、エラーメッセージの意味不明さには定評があります)。なるほど、確かにboot.iniファイルが消えてました。

修復するにはWindows XPをCDから起動して、回復コンソールを立ち上げます。画面の表示に従えば大丈夫だと思います。
回復コンソールが起動したら、以下のコマンドを入力。
bootcfg /list

これで起動設定(boot.iniファイルの中身)の一覧が表示されます。私の場合は、boot.ini自体が存在しないので、設定がありませんと怒られてしまいました。
設定が破損しているのを確認したら、以下のコマンドで修復を行います。
bootcfg /rebuild

途中、3回ぐらい選択肢が表示されますが、以下のように回答します。
インストールをブート一覧に追加しますか?
YES

ブートIDを入力してください
適当でかまいません。MacでBootCampしている私の場合はWindowsと入力しました。普通のWindowsマシンの場合にはWindows XP Professionalとか入れておくとわかりやすいのではないでしょうか。

オプションを指定してください
/fastdetect

これで完了です。あっさり終了するのでちょっと拍子抜け。あとはコンピューターを再起動すれば問題なくWindows XPが起動すると思います。

2009年5月4日月曜日

Objective-CのnilとNULLの違いって何?

自分用メモ。
nilは「Objective-Cの空のオブジェクト」、NULLは「C言語の空ポインタ」と解釈する。もっとも良い例がNSFileManagerのcreateDirectoryAtPath:withIntermediateDirectories:attributes:error:です。
http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Classes/NSFileManager_Class/Reference/Reference.html#//apple_ref/occ/instm/NSFileManager/createDirectoryAtPath:withIntermediateDirectories:attributes:error:

リファレンスを読んでみると、attributes:の指定が不要なときはnilを、error:の指定が不要なときはNULLを引数として与えろと明示的に書かれています。これは、attributes:が(NSDictionary *)型=NSDictionaryのオブジェクトを引数として受け取るのに対して、error:は(NSError **)型=NSErrorのオブジェクトのポインタを引数として受け取るためだと考えられます。といっても私はObjective-CもC言語もド素人ですので、ひょっとしたら大嘘かもしれません。違ってたらごめんなさい><


※追記:やっぱり定義そのものが違った><
すたっくおばふろ曰く(http://stackoverflow.com/questions/557582/null-vs-nil-in-objective-c
えらいひと曰く(http://www.libjingu.jp/trans/clocFAQ-j.html#objects-nil


知らないとついつい全部nilもNULLも同じだろと思いnilって指定してしまいそうですね。おそらくnilを指定しても動くとは思いますが・・・Objective-CとC言語の混ざり合いというか関わり合いがこんなところで垣間見れて面白いです。

Objective-CではJavaのようにNullPointerExceptionが発生したりしない

そう、Objective-CにはNilPointerExceptionのようなものがないんです。
Javaでは
int main(String[] args) {
  String str = null;
  int length = str.length(); //NullPointerException

  // bar()がnullだとNullPointerException
  Object result = Foo.bar().baz().abesi().hidebu();
}
Objective-Cでは
int main(int argc, char** argv){
  NSString *str = nil;
  int length = [str length]; //何も起こらない。lengthにはデフォルト値0が入る

  // barがnilを返したら、後続のbaz, abesi, hidebuもnilを返す。
  id result = [[[[Foo bar] baz] abesi] hidebu];
}

同僚の方(http://twitter.com/ksk_matsuo/status/1682599075)よりこっそり教えていただきました。ありがとうございます!

なんと言ってもJava人にとっていちばん恐ろしいのがこのNullPointerExceptionなのですよ。だからオブジェクトのメソッドを呼び出すときはいつでもオブジェクトがnullだったらどうしようガクブルと思いながらプログラム書いてるわけです。それがいきなり「Objective-Cになったらぬるぽなんてありません安心です^^」なんて言われても安心できないなぁ><

2009年5月2日土曜日

Pipeを使ってみた



つい最近知ったのですが、Yahooの運営するPipe(http://pipes.yahoo.com/)というサービスが最高に面白いです。


■そもそもPipeって何?
Pipeは、「数々のWebサービスが提供するRSSフィードなどのデータを簡単に加工して、別のRSSフィードなどのデータに変換して出力することができるサービス」です。「データ取得元」や、「繰り返し」、「文字列加工」や「URL生成」などのパネルをパイプでつないで、簡単なWebアプリケーションみたいなものを作ることができます。
もちろん無料です。必要なのは米Yahoo(http://www.yahoo.com/、日本のYahooではダメです)のアカウントだけ。
サーバー代金もいりません。好きなだけ新しいPipeを作ることが出来ます。


■さっぱりわからない。Pipeを使えば何が出来るのか具体的に知りたい。
たとえばこんなことが出来ます。
  • ニコニコ動画のマイリストIDを入力すると、マイリストのRSSフィードを生成して返すアプリを作ることが出来る。
  • ニコニコ動画のユーザーIDを入力すると、そのユーザーが公開している全動画のnicomimi(http://www.nicomimi.com/)へのリンクを自動的に生成してRSSフィードとして返すアプリを作ることが出来る。
  • ニコニコ動画の本日のランキング動画一覧から、タイトル・タグ・本文のいずれかに「東方」が含まれていて、再生回数が2万を超えている動画だけを抜き出してRSSフィードとして返すアプリを作ることが出来る。
ね?便利でしょ?


■わかったけど、それって難しくない?
では試しに、ニコニコ動画のマイリストIDを入力すると、マイリストのRSSフィードを生成して返すアプリを作ってみましょう。
Pipe(http://pipes.yahoo.com/)にアクセスして、Yahoo.comのIDでログインして、Create pipeってボタンを押して、何も考えずに左のサイドバーから以下のようにパネルを並べます。


簡単に説明すると、
1.マイリストIDをユーザーにテキストとして入力させて、
2.入力値を元に、ニコニコ動画のマイリストRSS取得用のURL(http://www.nicovideo.jp/mylist/5221166?rss=atom)を生成して、
3.URLからRSSフィードを取得して、
4.そのまま出力しているだけです。

実行結果はこちら。


ばっちりです。所要時間10分。サーバーの設定もデプロイも面倒なコーディングも一切いりません。デバッガも標準で付属(Pipe作成中にパネルをクリックすると実行結果がその場で見られる)。マウス操作だけで簡単に作れて、見た目がそれっぽいダイアグラムになるので、自分の作りたいWebサービスのちょっとした動作確認にも使える気がします。駆け出しのWebプログラマが、マッシュアップのセンスを磨いたりするのにも適していると思います。


■問題点
問題点もあります。主に日本人が使う上での問題点なのですが、
  • 全部英語
  • そもそも使いたいサービスがAPIやRSSフィードを公開していないと使えない
そう、使いたいサービスがAPIを公開していないケースが余りにも多い。ニコ動もキーワード検索結果がRSSで出ませんし、pixivに至ってはそもそもAPIの概念自体が無い。これでは「指定したマイリストIDのマイリスのうp主コメ中にpixivへのリンクが存在したらリンクをたどってpixivユーザーのIDを抜き出し画像全部RSSフィードとして取得する(ついでにお気に入りにも突っ込む)」みたいなアプリが作れません。逆にいえばAPIやRSSフィードのあるサービスであれば可能性は無限大です。サービスの提供している検索結果一覧やRSSフィードの内容が気に入らないなら、自分でPipeを書いて自分の好きなように調整すればよいのです。

Google App Engineで、index.yamlに記述したインデックスが正しく生成されないときの対処法



Google App EngineでDatastoreのindexを利用したアプリケーションを作成していたのですが、運悪くindexの生成がいつまで経っても終わらない現象に見舞われてしまいました。データ量はたかだか1000件程度しかないのですが、3日経ってもindexの状態が"Building"のまま。Googleグループの書き込みを見ると(http://groups.google.co.jp/group/google-appengine/search?group=google-appengine&q=index)、indexの生成に失敗するというケースがたびたび発生しているようです。
  • index対象となるモデルの数は全く関係がない。数件しか保存されていないモデルに対してindexを生成しても発生する。
  • Googleの中の人曰く、indexはアプリ開発者全体で共有のindex作成クローラーによって行われているため、全体的に一度に利用する人が増えると不安定になる。

■まずはappcfg.pyを利用して、indexを消して再生成してみる
まずはindexをいったん消して再作成することにより、自分の手で解決できないかどうか試してみます。こちらの記事(http://osima.jp/blog/gae-index-in-trouble.html)を参考にさせていただきました。

まずは、index.yaml中のトラブルを起こしているindexをコメントアウトして削除し、
# Get a list of articles for "all" news page.
#- kind: Article
# properties:
# - name: is_duplicated
# - name: date_update
# direction: desc

ターミナルから以下のコマンドを実行。
appcfg.py --force vacuum_indexes /path/to/my/application

うまくいけば、管理者コンソールのIndexesメニューを開くと、対象のindexが"Deleting"という状態になっているはずですので、しばらく待って完全にindexが消えてからindex.yaml中で再度コメントアウトした記述を復活して、
appcfg.py update_indexes /path/to/my/application

を実行すれば、無事にindexが再作成されます。


■それでもだめなら、GoogleグループからGoogleの中の人にお願いして消してもらう
これでうまくいけば万事解決なのですが、ときどきこの方法ではうまくいかない場合があります。たとえば私の場合は、下記のようなエラーが出て上手くindexの削除が出来ませんでした。
akisute $ appcfg.py --force vacuum_indexes .
Fetching index definitions diff.
Deleting selected index definitions.
2009-05-02 12:48:33,476 WARNING appcfg.py:670 An index was not deleted. Most likely this is because it no longer exists.

kind: Article
properties:
- name: is_duplicated
- direction: desc
name: date_update

このような場合は、2009/05/02現在、米国本家のGoogle App Engineグループ(http://groups.google.co.jp/group/google-appengine)に直接お願いしてGoogleの中の人によってindexを削除してもらうしかないようです。以下の点に気をつけて新規ディスカッションを投稿すれば、スムーズにindexを削除してもらえると思います。
  • indexの生成がBuildingのまま進まない、24時間以上経っているのにうんともすんとも言わないことを明記する
  • appcfg.py vacuum_indexを試したけれどもうまくいかなかったことを明記する
  • 自分のアプリのApp IDを載せる
  • どのindexを削除してもらいたいかを載せる
参考までに、Google App Engineグループをindexで検索したときの結果(http://groups.google.co.jp/group/google-appengine/search?group=google-appengine&q=index)を載せてみました。同様の症状例がたくさん報告されていますので、それらの文面を適当にパクって参考にして投稿するのをおすすめします。
ただし、
Before posting, please read our Charter. Please note that due to recent spam activity, a member's first post will be moderated by one of the group's managers.
と注意書きがあるとおり、現在Spam対策として、最初の1回目の投稿はモデレータによってチェックされてしまい、すぐに投稿が反映されないみたいです。投稿されたとしても、いつindexの削除をしてくれるかはGoogleの中の人の気まぐれになってしまうので、解決には少々時間がかかります。


■英語が苦手なら、日本のコミュニティに助けてもらおう!
どうしても英語が苦手なら、日本のGoogle App Engineコミュニティ(http://groups.google.co.jp/group/google-app-engine-japan)に相談してみるとよいかもしれません。たとえば、こちらの投稿(http://groups.google.co.jp/group/google-app-engine-japan/browse_thread/thread/289b87a344715ea1/c9b5f8e394ac6a47)が参考になります。


■でも本当は
早くこんなトラブルが起こらないようなシステムになってほしいです><