2010年6月20日日曜日

iPad を紙の代わりにするのに最適なアプリとスタイラスを探してみた



iPad を購入された皆さんが苦労されているのが「iPad の使い方を探す」事だと思います。私の場合は iPad を購入したら紙のシステム手帳を鞄から取り除いてしまいたいと考えていました。そのためには、カレンダーや連絡先はともかく、手書き機能が必要です。そこで購入直後からいろいろなアプリとスタイラスを買って(場合によっては作って)試行錯誤し、ようやくある程度の結論が出せたのでご紹介してみようと思います。

2011/05/29追記 - 記事全体の構造を再編成して、より分かりやすく、現状に即した形に書き換えました。私がこの記事を書き始めたころと比べ、スタイラスの性能もアプリの性能も飛躍的に向上し、どれを選んでもほぼ間違いないレベルにまで進化いたしましたので、そろそろまとめにして最終更新にするつもりです。
2011/02/01追記 - パワーサポート スマートペンを追記。
2010/10/10追記 - oStylusのレビューへのリンクを張りました。


実際に手持ちのスタイラスで書き比べてみた記事がこちら: http://akisute.com/2011/05/iphone-ipad-2.html
Bamboo Stylusのレビューはこちら: http://akisute.com/2011/05/ipad-bamboo-stylus.html
oStylusのレビューはこちら: http://akisute.com/2010/10/ipad-ostylus.html


■まず最初に結論

機能性、入手容易性、使い勝手、値段などを考慮した結果、2011/05/29現在私がオススメするアプリとスタイラスは以下のものです。それでは個別に見ていきます。


■アプリ編

手書きアプリには大きく分けて以下の三種類があると思っています。
  1. ノート型手書きアプリ
    • 紙のノートの代わりに使うための手書きアプリです。実際のノートのように背景に罫線が入っていたりする場合もあります。
    • 機能が絞られており、特にペン先は一種類しかない場合が多いですが、その代わり実際にペンで書いているかのような美しい線が書けます。
    • 拡大縮小は限定的にしかできません。
    • 見た目に非常にこだわっているアプリが多いです。
  2. お絵かき型手書きアプリ
    • キャンバス全体のスクロールや拡大縮小ができる手書きアプリです。どちらかというと、紙のキャンバスやホワイトボードを置き換えるアプリです。
    • 図形や絵を書くのに適しています。
    • 絵を描くのに必要な機能が多いです。たとえば色が多いとか、フィルタがついているとか、レイヤが使えるとか、ブラシの種類が多いとか。
  3. その他、独創的なもの
    • 全く新しい使い方を提案するアプリです。
    • 7notesのように手書き自動認識をしたり、Instavizのようにグラフを自動で作ってくれたりするものがあります。
ここでは私自身が紙のノートとホワイトボードの代わりを欲しているということで、キーボードでタイプする機能を持っているアプリについては除外しており、純粋に手書き機能のみで、かつ私が実際に試してみたことのある 1. と 2. の一部についてのみご紹介いたします。あらかじめご了承ください。

neu.Notes - お絵かき型

  • 良い点は、なんといっても無料で、ホワイトボードの代わりをするのに必要な機能は全てある。書き味も悪くない。
  • 悪い点は、デザインがイマイチ、ノートのページが増えてくるとだんだん重くなってきて書き味が劣ってくる。
これだけ使えるアプリが無料というのが何か間違ってる気がしますが、それぐらい素晴らしいです。とりあえず入れておいて損はありません。

Adobe Ideas - お絵かき型

  • 良い点は、neu.Notesよりも綺麗な書き味と使っていてしっくりくる美しいインターフェース。レイヤーも使える。
  • 悪い点は、高い(特に日本のストアでは)、ノートの整理が全く出来ないため数が増えてくると真っ先に破綻する、外部連携が弱い。
高いのが問題ですが、使っていてneu.Notesとどちらがイライラしないかと問われると、やはりこのAdobe Ideasですね。

Penultimate - ノート型

  • 良い点は、シンプルで必要最小限の機能だけを含んでいるところ。優れた書き味、比較的安い値段。
  • 悪い点は、シンプルすぎて出来ることが少ない、(ズームが一切出来ないのが致命的)、ノートの整理がイマイチ、ここ最近のアップデートの内容がイマイチで良い方向にアプリが改善される気配がない、外部連携が弱い。
昔はノートと言えばPenultimateというぐらい良かったんですが、最近は後述するNoteshelfやNoteTaker HDが見事なぐらい成長してしまったため、もはや過去の遺物という印象です。安いぐらいしかメリットがないです。

Noteshelf - ノート型

  • 良い点は、美しいUI、必要な機能は全てある上でよけいな物がないシンプルさ、ズームが出来て紙のノートと同じように細かく書ける、Penultimateと同等レベルの優れた書き味、そして何より外部連携が神。Dropboxは当然として、Evernoteにも連携可能。書いたノートをPDF形式で選択したページだけEvernoteにアップロードなどと言うことが余裕で出来る。あとはEvernoteにOCRしてもらってあとから検索する・・・
  • 悪い点は、 正直見当たらない。 いや本当に。自分のやりたいことが今のところ全部出来ているし、非常に安定していて1ノートに画像込みで40ページ以上追加しても性能劣化やクラッシュが発生しない。紙のノートみたいに200ページぐらいのノートを作っても大丈夫そうな勢いがある。
あの孫正義が絶賛!とか紹介に書いてあって超うさんくせぇと思っていたのですが、すみません、私が間違っておりました。本当にこのアプリは素晴らしいです。紙のノートを一冊買うぐらいなら、今すぐこちらを買ってください。

ここでご紹介した以外にも、たとえば むげんメモ ですとか Note Taker HD といったアプリが面白いと思いますが、実際に使っていないので評価は控えさせていただきます><


■スタイラス編

2011/05/29編集 - 近況に合わせて完全に書き直し。

スタイラスについては、時代別に見ていきます。

原始時代(2008年~iPad発売前後)

この時代に存在したiPhone用スタイラスと言えば、ほぼ全てが先端が黒い平らなゴム状になっているスタイラスです。例を挙げればこのような商品です。
http://www.ray-out.co.jp/products/t1pen1/

残念ながら魚肉ソーセージの方がマシなので、何を間違っても絶対に買わないでください。これらの原始時代スタイラスは、導電性の低い素材で出来た平らな先端を、無理矢理押しつけてタップすることしか考えられていないため、手書きは不可能です。

さすがに最近は第二世代が登場してきたおかげで、ほぼ市場から駆逐されたようです。

第一世代(iPad発売~iPad発売後9ヶ月程度)

とまぁiPadが発売されるまではほとんど暗黒時代だったのですが、iPadが登場してきたあたりから私が第一世代と呼んでいるレベルのスタイラスが登場し始めました。すなわち、
  • 何でもいいからとにかく引きずって手書きできる先端
  • とりあえずまともに使えるレベルの導電性
を持っているスタイラスです。主に先端が導電性スポンジで出来ているスポンジ型と、筆のようになっているブラシ型が主流でした。中には変わり者で先端が金属で出来ている物もあります。これでとりあえず手書きは出来るようになりましたが、第一世代のスタイラスは値段が高く、世間一般にあまり流通していないためAmazonなどで気軽に買えず、その割には第二世代と比べて性能が悪すぎるので、今はもう忘れて結構だと思います。一応列挙すると、
  • Pogo Sketch - 第一世代の中で飛び抜けて優秀だったスタイラス。導電性以外は今でもほぼ完璧
  • oStylus - 金属を使うことで素晴らしい導電性を確保したスタイラス
などがあります。

第二世代(2011年~)

そして2011年になってついに転機が。パワーサポート スマートペン PBJ-9Xシリーズの登場を皮切りに、第二世代と呼ぶにふさわしいスタイラスが次々と登場し始めました。

第二世代の特徴は、
  • 日本のAmazonで3000円以内で余裕で買えるため入手しやすい
  • 触れるだけで反応する、極めて高い導電性と感度
  • 普通のペンと全く同じように引きずって手書きできるスムースなペン先
などを兼ね備えているところで、ようやく誰でもいいスタイラスを簡単に入手できるようになりました。この世代のスタイラスはほぼ全て中空ドーム状の柔らかいシリコンをペン先に採用しているため、一目でわかります。

以下、実際に買って試したことがあるものを挙げてみます。

パワーサポート スマートペン PBJ-9Xシリーズ
  • 良い点は、驚異的な先端感度と、安めの値段。本体が軽いのも魅力。
  • 悪い点は、先端の強度にやや不安があること、柔らかくて太いため正確に書けず、少し引っかかりがあること。
おそらく最初に出てきた第二世代のiPhone/iPad用スタイラスだと思います。今でもまったく見劣りしません。今では他にもっと安い商品もあるようですが、500円程度しか変わりませんし、あまり神経質にならなくても良いかと思います。

Wacom Bamboo Stylus
  • 良い点は、細くてスムーズなペン先、本物のペンのような筆感。
  • 悪い点は、やや重い本体と、多少感度の悪い先端。
詳細なレビューはhttp://akisute.com/2011/05/ipad-bamboo-stylus.htmlをご覧ください。高いだけあって良いです。

プリンストンテクノロジー iPad/iPhone/iPod touch専用タッチペン(ブラック) PIP-TP2B
  • 良い点は、十分な感度と、スマートペンより固くしっかりしていて書きやすいペン先。軽くて疲れない軸。値段も相当安い。ストラップを通すための穴がある。
  • 悪い点は、軸が短い。女性にはちょうどいいかもしれませんが、ただでさえ軸が短めな上にクリップも大きいので、手の大きい人には不向き。
ちょうどパワーサポートのスマートペンとBamboo Stylusの中間のようなペン先です。軽くて感度がよいあたりはBambooより優れており、ペン先が固くてふにゃふにゃしないのがスマートペンより優れていて、さらに安いのがメリット。一方でペン軸が短め(Bamboo Stylusと比べると1cmも短い)という別の問題もあり、他の製品と比べて一長一短ありますが、オススメです。

その他にも最近ではなどがあるようですが、これらの商品は実際に買って試せていないのでここでは評価を控えさせていただきます><

2010年6月19日土曜日

NSOperation を使って外部 API から非同期に結果を取得してみる

iPhone / iPad のアプリを作っていると、頻繁に登場するのが「外部 API を HTTP 経由で実行して結果を XML / JSON で取得し、それを解析してモデルクラスに変換してデータ構造に突っ込む」パターンです。当然たくさんの先人の皆様がすでに効率的なライブラリを作成されているのですが、あえて私も車輪の再発明に挑戦してみました。今回使用したのは NSOperation クラスです。 NSURLConnection クラスとデリゲートを使うだけでも簡単に非同期通信を実現することができるのですが、さらに NSOperation クラスと NSOperationQueue を使うことでさらにタスク間の依存関係を簡単に設定できたり、タスクの並列度を簡単に制御したりできそうなので、挑戦してみました。

2010/12/29追加: 発展版をASIHTTPRequestを使って作成してみました。


■実装コードと使い方

http://gist.github.com/441620

これが基底クラスとなる FetchOperation クラスです。使用する際にはこのクラスのサブクラスを作成して、以下のように parseResponseBody: メソッドをオーバーライドします。
@interface FetchMyAPI : FetchOperation
@end

@implementation FetchMyAPI
- (void)parseResponseBody:(NSData *)bodyData {
  NSString *string = [[[NSString alloc] initWithBytes:[bodyData bytes] length:[bodyData length] encoding:NSUTF8Encoding] autorelease];
  JSON *json = [MYJSONLibrary jsonFromString:string];
  // json を解析してモデルクラスに突っ込むなど・・・
  // parseResponseBody: メソッドはtry-catchで囲まれており、さらに Core Data Managed Object Context に対して lock をかけています
}
@end
あとはこのサブクラスのインスタンスを生成して、requet プロパティに任意の NSURLRequest を渡して、普通の NSOperation を使うときのように KVO を使って状態を監視すればOK。
FetchOperation *operation = [[[FetchMYAPI alloc] init] autorelease];
operation.request = myAPIRequest;
[operation addObserver:self forKeyPath:@"isCancelled" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
[operation addObserver:self forKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
[appDelegate.operationQueue addOperation:operation];
もちろん NSOperation のサブクラスなので、 FetchOperation 同士に依存関係をつけるのも簡単です。たとえばログインAPIを実行した後にゲットユーザーAPIを実行したい場合には、
[getUserOperation addDependency:loginAPIOperation];
などとして、おなじ NSOperationQueue に突っ込んでやるだけで、自動的にログインAPI→ゲットユーザーAPIの順に並列実行してくれます。超便利です。

上記コードは基本的にご自由にお使い頂いて結構です。ただし、そのままコピペしても NSOperationQueue がなかったり Core Data Managed Object Context がなかったりで、まず間違いなく動かないと思います。適当にご自身の環境に合わせて調整していただければ幸いです。テストは結構行っているので多分大丈夫だと思うのですが、バグとか残っているかもしれません。何かあっても責任は持てません、ごめんなさい><


■NSOperation クラスのサブクラスの作り方

さてここからは実装のお話です。基本的には iPhone SDK についてくるリファレンスの NSOperation のページに全ての注意事項が載っているため、こちらを注意深く読んで実装すれば難しいことはありません(全部英語ですが><)。

最初に覚えておくべき知識として、 NSOperation クラスには二通りの実行モードが有ります。 以下、iPhone OS 3以下での挙動です。 iOS 4以降は挙動が変わり、Mac OS X 10.6以降と同じ挙動になります。
  • 非並列実行モード
    • isConcurrent プロパティが NO を返すとき、またはiOS 4以降は isConcurrent プロパティの値に関係なく常にこちらのモードになります
    • NSOperationQueue クラスが自動的に新スレッドを1本作って、そこで実行してくれるので、なにも考えなくても並列処理になります
    • main メソッドの実行が完了したら自動的に処理が完了したとみなされます
  • 並列実行モード
    • isConcurrent プロパティが YES を返すとき
    • NSOperationQueue クラスは自分のいるスレッドで NSOperation を実行します。そのため、 NSOperation 側で並列処理を行わないとスレッドが固まります
    • main メソッドの実行が完了しても処理完了とはみなされません。自分で明示的に処理が終わったことを通知できるようにする必要があります
簡単に実装できるのは非並列実行モードなのですが、これで実装するためには NSURLConnection を同期モードで実行する必要があります。で、困ったことに NSURLConnection の同期モードはひどい実装で処理も遅ければメモリもムダ食いするしタイムアウトの時間すら自分で決められないという有様なので、まったく使えません。そのため今回はやむを得ず並列実行モードを頑張って実装しました。

並列実行モードを自分で実装する手順は以下の通り。

まず何はなくとも isConcurrent で YES を返すようにします。
- (BOOL)isConcurrent {
    return YES;
}
続いて以下の4つの状態通知メソッドを実装します。これはコピペでOKですが、ready, executing, finished, cancelledは自分で宣言してください。
- (void)setReady:(BOOL)b {
        if (ready != b) {
                [self willChangeValueForKey:@"isReady"];
                ready = b;
                [self didChangeValueForKey:@"isReady"];
        }
}
- (void)setExecuting:(BOOL)b {
        if (executing != b) {
                [self willChangeValueForKey:@"isExecuting"];
                executing = b;
                [self didChangeValueForKey:@"isExecuting"];
        }
}
- (void)setFinished:(BOOL)b {
        if (finished != b) {
                [self willChangeValueForKey:@"isFinished"];
                finished = b;
                [self didChangeValueForKey:@"isFinished"];
        }
}
- (void)setCancelled:(BOOL)b {
        if (cancelled != b) {
                [self willChangeValueForKey:@"isCancelled"];
                cancelled = b;
                [self didChangeValueForKey:@"isCancelled"];
        }
}
次に start メソッドを適当に実装します。
- (void)start {
        // Follows the behavior of NSOperation in Mac OS 10.6 (and iPhone OS 3.0)
        // また、start前にrequestがセットされていない場合にも例外を発生させる
        if (finished || cancelled) {
                [self cancel];
                return;
        }
        if (!ready || executing || !request) {
                @throw NSInvalidArgumentException;
        }

        // ここで適当に事前処理を行う
        // ここは同期実行でいいです、すぐ終わるなら

        // main実行直前に現在の状態をexecutingにする
        [self setReady:NO];
        [self setExecuting:YES];
        [self setFinished:NO];
        [self setCancelled:NO];

        // mainを実行する
        [self main];
}
最後に主処理を main メソッドに書きます。 main メソッド自身か、または main メソッドの大部分は並列実行されるようにしてください。
主処理が完了したら以下のように自身の状態を更新すればOKです:
[self setReady:NO];
[self setExecuting:NO];
[self setFinished:YES];
[self setCancelled:NO];
必要に応じて cancel メソッドも実装します
- (void)cancel {
        // ここで自身の主処理をキャンセルする

        // 現在の状態をcancelにする
        [self setReady:NO];
        [self setExecuting:NO];
        [self setFinished:YES];
        [self setCancelled:YES];
}


■FetchOperationの解説

以下のような順番で処理が進みます。
  1. FetchOperation のオブジェクトが生成される (init)
  2. FetchOperation のオブジェクトが NSOperationQueue に追加される (start)
  3. 並列実行モードで FetchOperation が実行され、 start メソッドが main メソッドを呼び出す (main)
  4. main メソッドの中で NSURLConnection が非同期実行される (main)
  5. NSURLConnection がレスポンスを受け取って、データを受信する (connection:didReceiveResponse:)
  6. NSURLConnection がすべてのデータを受信する (connectionDidFinishLoading:)
  7. NSInvocationOperation クラスを使って受信したデータの解析処理を並列実行する (parseMain:)
  8. parseMain: の終了を監視して、終了し次第 FetchOperation の実行を完了する (observeValueForKeyPath:ofObject:change:context:)
大きく分けると、1〜3が事前準備、4〜6が通信部、7〜8が解析部になっています。7〜8は NSInvocationOperation を使ってお手軽に並列実行していますが、別の NSOperation にしたほうがより良い実装になると思います。現実装のように NSInvocationOperation を使っていると、何かの間違いで FetchOperation 自身に複数のスレッドからのアクセスが発生して状態が壊れてしまう恐れがあります。

isConcurrent プロパティで YES を返すようにしているため、 FetchOperation 自身は NSOperationQueue が置いてあるスレッドと同じスレッドから実行されます。なので、実際に並列実行されるのは NSURLConnection が通信をしている部分と、 parseResponseBody の中だけです。それ以外の部分で重い処理を実行すると思いっきり固まるのでご注意ください。

2010年5月30日日曜日

Core Data のパフォーマンスをちょっとだけ調べてみた

ちょっと仕事で触ってみて分かった範囲のことを書きます。断りがない限り、 iPhone 3GS で Wifi 接続環境下においてテストしました。

■キャッシュ無し vs キャッシュ有り

executeFetchRequest:error: メソッドを用いて、 Entityのプロパティで一件だけ絞り込んで返すようなクエリは大変遅いということが分かりました。Indexを付けて実行してもほとんど速くなりません。どうやらそもそもバックエンドに使っているSqliteが大変遅い、特にコネクションを生成したり破棄したりするのが遅い感じがするので、ループで一件ずつ取得するなどのときはたくさんのSQLが実行されないようにする必要があります。 objectWithID: メソッドは試していないのでちょっと不明です。

回避策として、アプリが起動したタイミングで当該エンティティの全オブジェクトをあらかじめ取ってきて、 NSMutableDictionary にでも突っ込んでおく。次回以降のフェッチはその NSMutableDictionary から行う、と言うようにすると凄く速くなりました。

実測値は以下の通り。
pre loading time というのが自前のNSDictionaryキャッシュの事前生成、parseResponseBodyというのがXMLの解析で、この中に大量のCore DataオブジェクトをDBから引っ張ってくる処理が含まれています。
//////////////////
プリキャッシュなし
//////////////////

2010-05-25 12:11:37.254  FetchAllModelA parseResponseBody time : -1.214856
2010-05-25 12:11:39.674  FetchAllModelB parseResponseBody time : -2.416063
2010-05-25 12:11:41.097  FetchAllModelC parseResponseBody time : -1.384185

//////////////////
プリキャッシュあり
//////////////////

2010-05-25 14:12:14.788  pre loading time : -0.039540
2010-05-25 14:12:17.518  FetchAllModelA parseResponseBody completed. time : -0.836754
2010-05-25 14:12:20.719  FetchAllModelB parseResponseBody completed. time : -1.312910
2010-05-25 14:12:19.169  FetchAllModelC parseResponseBody completed. time : -0.902832
ほんの0.03秒のプリキャッシュ処理のおかげで、XML解析が最大で1秒以上短縮できています。とにかくCore DataがSQLを飛ばさないように調整すると効果があるみたいです。


■DB書き込み速度

NSManagedObject の生成はメモリ上 (NSManagedObjectContext) で行われるためなかなか高速なのですが、それを save するのがとにかく iPhone 3G で顕著に遅く、 300件程度のデータを保存するのに5秒以上かかってアプリが正常終了できないという事態が発生しました。リレーション張りすぎたかなーと思います>< iPhone 3GS なら2秒3秒程度で間違いなく完了するので特に問題になっていません。対策としては暇なときに逐一DBにsaveするか、どうでもいいデータはiPhone 3Gでは保存しないとか、一時エンティティにするとか。

iPhone の NSURLRequest で gzip 使ってみた

http://stackoverflow.com/questions/2682483/nsurlconnection-nsurlrequest-gzip-support

こちらに記載があるとおり、 Cocoa の NSURLConnection はgzip圧縮されたコンテンツを自動的に解凍して扱ってくれるみたいです。gzipでリクエストを受け取るには、以下のように NSURLRequest のヘッダに値を追加します。
[urlReq setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
これだけで後はすべて自動的にやってくれます。もちろんサーバー側がきちんとgzip圧縮して返すように設定されていなければなりません。


■パフォーマンスを試してみる

サーバーと通信を行い、レスポンスとして1000行程度のXMLを取得して解析するアプリで実験してみました。iPhone 3GS, Wifi回線を使用。

圧縮無しの時の結果はこちら。
API①
16:11:58.822 開始
16:12:04.126 レスポンス受信
16:12:04.184 データ受信
16:12:05.322 パース完了

API②
16:11:58.838 開始
16:12:04.118 レスポンス受信
16:12:04.433 データ受信
16:12:07.276 パース完了

API③
16:11:58.847 開始
16:12:04.718 レスポンス受信
16:12:05.822 データ受信
16:12:08.299 パース完了
圧縮ありの時の結果はこちら。
API①
16:03:43.056 開始
16:03:46.349 レスポンス受信
16:03:46.414 データ受信
16:03:48.473 パース完了

API②
16:03:43.080 開始
16:03:46.326 レスポンス受信
16:03:46.671 データ受信
16:03:49.905 パース完了

API③
16:03:43.088 開始
16:03:46.308 レスポンス受信
16:03:46.371 データ受信
16:03:47.532 パース完了
一回しか試していないのでムラはありそうですが、通信自体は確実に早くなっている気がします。3Gで計測すればさらに顕著な違いになると思うのでおすすめです。

iPad 3G を Xcode につないだら対応していない OS と言われてしまった件



昨日の勉強会でさっそく買ったばかりのiPad 3GをXcodeにつないでみたら、ご覧のように対応していないOSであるとエラーが出てしまいました。

再度iPhone Developer CenterからSDKをダウンロードしてインストールし直したら問題なく動作するようになりました。おそらく単にSDKが古かっただけなのだとは思いますが、一応。

第 2 回西東京 iPhone 勉強会に行ってきました

http://atnd.org/events/4014

メンツがそうそうたるメンバーでびっくりしました。内容も非常に充実していて、 iPhone 技術ネタとしては Bonjour と UIGestureRecognizer 、iPhone 技術以外のネタとしては iPhone アプリのネタだしの仕方と iPhone アプリで受託案件をした際のお話、最後に HTML5 で Keynote みたいなのを作ったよと言う話でした。

技術ネタは予習しておいたおかげで、聞くだけになってしまわなくてすごくよかったです。 UIGestureRecognizer は UIView.multiTouchEnabled を無視して複数指のジェスチャが取れるというネタが大収穫。あとは UIWebView のタッチを無理矢理検出する方法とか。

個人的には iPhone アプリで受託案件をした際の経験談が非常に面白かったです。作る方はまだ分かるのですが、契約だとかお金の話だとかそういう話になってくるとなかなか参考になるものがないので、非常に貴重でした。次回もこのネタは聞きたいです。

HTML5 は可能性を見た感じです。マルチタッチも検出できているし簡単なアニメーションも CSS で実装できているみたいで、 JavaScript (笑)なんて言えなくなりそう。

予想通りというか iPad 率は6割超えてました。 iPhone アプリリリース経験者も5割以上。

最後に懇親会で iPadによる iよせがき を試してみました。はい、単に岸川さん高山さんにiPad上にサインを貰っただけです>< ありがとうございます!

2010年5月24日月曜日

Core Data では モデルの delete - insert をしない方が良い

最近お仕事で Core Data を頻繁に使っているのですが、ちょっとだけハマったケースをご紹介します。 Core Data ではモデルの delete - insert による更新をしない方が良いようです。


■delete - insert が問題になるケース

たとえば、 Core Data から全く同じ内容をフェッチして表示する二つのUITableViewがあるとします。それぞれ A および B と名付けます。
ここで、 A を表示してから、 B を表示する際に Core Data モデルを delete - insert すると、 A を再度表示した際にアプリケーションがクラッシュします。原因はおそらく、 A で既に読み込まれているモデルを B が削除してしまっているため、 A で既に削除されているモデルをテーブルが表示しようとしてクラッシュしているものだと思われます。 A の viewWillAppear などのタイミングで、再度 Core Data からオブジェクトを取得し直せばこの問題は解決します。

そもそも NSFetchedResultsController を使えばたぶんこの問題は発生しないと思うのですが、いずれにせよ変更があったデータをフェッチしなおさなければならなくなってしまいます。ということで可能な限りプライマリキー項目を設定して update で対応することをお勧めします。ただし update は update で sqlite3 の実行がやたら遅いということが分かっているのでこちらはこちらで別途チューニングが必要です。詳細はまた別エントリーにて。

2010年5月4日火曜日

Byline 3 ヘルプ と Tips

Byline 3 のちょっとした日本語ヘルプとTipsをこちらに掲載しようと思います。

※2010/05/11よりBylineの日本公式アカウント @bylinejp を開設しましたので、是非ご活用ください。


■メインフォルダって何?

設定画面の同期セクションにあるメインフォルダという設定。これは、簡単に言えば Bylineで表示するフォルダ です。デフォルトでは「すべてのアイテム」になっています。この状態だと、 Google Reader 側にあるすべてのフォルダがBylineと同期されて画面に表示されます。メインフォルダを設定することで、 Google Reader 側の指定されたフォルダのみをBylineと同期して表示する事ができます。一部のフィードのみを同期したい場合に有効です。

注意点として、 一度に指定できるメインフォルダは一つだけ です。


■Webページのキャッシュ設定について

設定画面のキャッシュセクションにあるWebページという設定。こちらの内容が少々難解になっているので解説したいと思います。


  • モバイル向けデザイン
    この設定をONにすると、外部Webサービスを使用して対象のWebページのコンテンツを「モバイル向けデザイン」に変換してキャッシュします。モバイル向けデザインにすると、CSSが除去されたり、イメージが除去されたりリサイズされたりします。また、全体的に縦長なデザインになり、モバイルデバイスで読みやすい形式になります。
  • 自動でキャッシュするコンテンツ
    メインフォルダ、スター付きアイテム、メモのうち、同期時に自動的にキャッシュするものを選択します。画像はメインフォルダが「すべてのアイテム」にセットされている場合の物ですが、メインフォルダを変更するとここの名前も変化します。この設定項目をONにした場合、以下のようなポリシーでキャッシュが行われます。
    • メインフォルダに含まれるフィードは、次の「フィードごとのキャッシュ設定」で設定した内容に従ってキャッシュされます。
    • スター付きアイテムに含まれるフィードは、すべてのフィードがキャッシュされます。
    • メモに含まれるフィードは、すべてのフィードがキャッシュされます。


  • フィードごとのキャッシュ設定
    メインフォルダに含まれるフィードのキャッシュポリシーを設定します。 この設定が適用されるのはメインフォルダに含まれるフィードのみです。 スター付きアイテムおよびメモには適用されず、常にすべてのフィードがキャッシュされます。
    一覧にある各フィード名をタップすると、以下の三つの状態が順番に変化します。
    • 無印(デフォルト状態)
      本文が意図的に削られていると判断されたアイテムのみを自動的にキャッシュします。すべての本文が含まれているアイテムはキャッシュしません。
      本文が意図的に削られているアイテムというのは、たとえば このような ニュース本文全体を乗せない RSS フィードの記事のことです。
    • √マーク付き
      本文が削られているか否かに関わらず、常にキャッシュします。
    • ×マーク付き
      本文が削られているか否かに関わらず、常にキャッシュしません。
    また、右側の図にある薄い色の√マークがついているフィードは、「Bylineによって本文が意図的に削られていると判断されたフィード」であることを示します。この薄い色の√マークはメインフォルダを自動でキャッシュする設定にしているときにのみ表示されます。左側の図のCNET Japanにはチェックが無くて右側の図のCNET Japanにはチェックがあるのはこれが原因です。


■Twitter連携の設定方法

BylineではOAuthを使用してTwitterアカウントにログインします。残念ながらこのOAuthの画面が英語のままになっているため、こちらで代わりまして簡単に解説いたします。



設定画面のアカウントセクションにあるTwitter欄をタップすると、上のような画面になります。ユーザー名とパスワードを入力して、ログインボタンを押してください。



この画面が正常に表示されれば完了です。画面右上の「完了」をタップして終了してください。なんか下の方で Redirecting you back to Byline とか表示されちゃってますが無視してOKです。


■Twitter投稿時にURLを短縮する方法

Twitter投稿画面には一見、URLを短縮するためのボタンがありません。ですがご安心ください。BylineにはURLを自動的に短縮する機能があります。



このように普通に本文をタイプして、140文字よりも長くなってしまうと・・・



ご覧の通り!自動的にURLが短縮されます。



さらに本文が長くなってそれでも投稿できなくなってしまったらこのようになります。どうでしょうか、いちいちボタンを押して縮めるよりスマートだと思います。


■裏技:Byline 2と同じ感覚で「すべて既読にする」方法

Byline 3ではUIが変更されて、Byline 2ではアイテム一覧の一番下にあった「すべて既読にする」ボタンがありません。個人的にはこの変更はすごい抵抗があって作者の人にも何とかならないかとお願いしてみたのですが、その代わりにちょっとした裏技を付けてくれました。



画面一番下のツールバーをスワイプすると、ツールバーに「すべて既読にする」ボタンが表示されて、すべて既読にすることができます。これで多少はByline 2に近い感覚で操作できるかと思います。

2010年5月3日月曜日

Byline 3 の新機能まとめ



ちょっとした宣伝ポストになりますが、私が日本語翻訳を手がけてます iPhone の RSS リーダーアプリ Byline のバージョン3が近日リリースされます。すでにリリース候補ビルドが完成しており、特に何の問題もなければ来週月曜日 (5/10) にリリースされる予定です。既存の Byline 2 ユーザーの方はそのまま無料でアップデートすることが出来ます。

バージョン2にくらべて高速で動作し、文字エンコードの取り扱い方法を変更したため以前よりクラッシュしない・・・はずです。少なくとも最新バージョンになってからは私のクライアントでは一度もクラッシュしておらず、安定性は上がっていると思います。

Byline 2 からの変更点とか新機能とかを以下にまとめてみました。


■Google Reader との同期速度向上

Google Reader と同期する際のスピードがさらに高速化しました。特にすべてのフィードのインデックスを生成する速度が劇的に改善されています。具体的にどれぐらい早くなったのか、私の iPhone 3GS 上で実際に測定してみました。

Byline 3


Byline 2


使用したバージョンアイテム件数同期にかかった時間アイテム/秒
Byline 2.5.6511件340.43秒1.50
Byline 3.0f1275件135.99秒2.02

ごらんのように、およそ3割程度速度が向上していることがわかります。アイテム一件一件のキャッシュ速度はそれほど変わりませんが、最初のインデックス取得が劇的に高速化している分だけ全体にかかる時間が短縮されているようです。


■アイテムのキャッシュ機能強化

目玉であるWebページのキャッシュ機能も強化されています。今回のバージョンから、 途中で省略されているアイテムのみをキャッシュする ことが可能になりました。途中で省略されているアイテムというのは、たとえば このような ニュース本文全体を乗せない RSS フィードの記事のことです。 Byline はこのような省略されている記事のみを判定して自動的にキャッシュしてくれます。


■インターフェース変更

はい、 Byline のバージョンアップといえば毎度おなじみのUI変更とアイコンの変更です>< 2までの操作に慣れた方(自分含む)には申し訳ありませんが、また慣れてください・・・その代わり前のインターフェースより優れているところも結構あります。



メイン画面はこんな感じです。フォルダの中の個別のフィードごとに記事を読めるようになりました。



記事一覧。リストの一番下にあった「すべて既読にする」がなくなり、その代わり右上に「編集」ボタンがついてそちらから一度に既読に出来るようになっています。



記事詳細も変わっています。画面左下にあったキャッシュを見るボタンが右上に移動しました。



こんな具合に記事詳細を左右にスワイプすることでつぎつぎと記事をめくっていくことが出来ます。

■Twitter / Instapaper / Read it Later との連携



最近の RSS リーダーではおなじみの外部連携が強化されています。 Twitter / Instapaper / Read it Later と連携することが出来ます。写真は Twitter への投稿画面。結構良くできています。リンクが長すぎる場合には自動的に短くしてくれたりします。


■まとめ

最近は Byline よりも多機能でより多くの外部サービスと連携できる RSS リーダーが登場してきていますが、今回のアップデートで外部連携が強化されたり個別のフィードを読めるようになったりと、それらの最新アプリと対抗できるだけの機能がついてきたかなと思います。目玉の Web ページキャッシュ機能もさらに強化されています。あとは安定動作してくれればまず負けないと思うのですが・・・こればっかりはリリースして皆さんに使っていただかないとわかりません >< とはいえ私の環境では500件以上のフィードを一度に読み込んでも安定しており、前バージョンよりさらに高速化しています。ご期待ください!

UTF-8 の文字列の長さを正確に求めるためには Normalize しましょう

Twitterにメッセージを送信する際に、クライアント側でメッセージの長さを判定して画面に表示したいというような要件があると思います。当然、このような場合には入力されたメッセージを取ってきてその長さを求めてやれば良いのですが、どうやら UTF-8 などのマルチバイトな文字コードを使っていると文字列の長さを正確に求めるのが大変なようです。


■何がどう大変なのか

図を書いてみました。「文」が文字数、「b」がバイト数を表しています。 Objective-C の場合は、「文」が ``[NSString length]`` を、「b」が [NSString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] を表します。ほかの言語の場合もそれぞれ相当するメソッドまたは関数が用意されていると思います。



上のケースが一般的な UTF-8 文字列です。この場合、バイト数はマルチバイトになってしまうため実際の文字数(我々人間が見て自然な文字数)とは一致しませんが、その代わり UTF-8 文字数が実際の文字数と一致するため問題なく長さを求めることができます。

面倒なのが下のケース。ウムラウト記号や濁点・半濁点が一文字として入力されてしまっている場合です。通常滅多にこのような文字列に遭遇することはないのですが、まれにこのような文字列を入力させる処理系があるようです。で、この場合はウムラウト記号や濁点・半濁点も一文字として認識されてしまうため、実際の文字数と文字列長判定メソッドが返す文字数が異なってきます。そこまで厳密にしなくてもよいのでは・・・と思ったのですが、なんと Twitter はきちんとこのような場合も自然な文字列の長さを測定して140文字かどうかを判断しているらしいです。


■対策:Normalizeする

このように実際の文字数と UTF-8 上の文字数が異なっている場合には、 Normalize と呼ばれる処理を行って文字を圧縮する必要があります。 Objective-C の場合には以下のようなメソッドが標準で用意されています。
// NFD
– (NSString *)decomposedStringWithCanonicalMapping

// NFKD
– (NSString *)decomposedStringWithCompatibilityMapping

// NFC
– (NSString *)precomposedStringWithCanonicalMapping

// NFKC
– (NSString *)precomposedStringWithCompatibilityMapping
これらのメソッドは、 Unicode 文字列をそれぞれ NFD, NFC, NFKD, NFKC と呼ばれるフォーマットの文字列に変換してくれます。それぞれの文字列がどのような表現を表しているかは、以下のページを参考にしてみてください。英語ですが図説がついてるので非常にわかりやすいです。
http://unicode.org/reports/tr15/
http://homepage1.nifty.com/nomenclator/unicode/normalization.htm

ほかの言語、たとえばPythonやRubyなどでこの機能が提供されているかどうかはわかりません。あしからず・・・><

2010年4月29日木曜日

Three20 の TTPhotoViewController でローカルファイルを扱う際の注意点

Three20 フレームワークを使ってアプリを作成しているときに、ローカルファイルを TTPhotoViewController に読み込ませようとして一部はまってしまったので共有します。


■結論から
  • TTPhotoViewController の中に一部 NSHTTPURLResponse でないと動かないところがあるため、ファイル読み込みでは動作しない
  • @"bundle://filename.jpg"@"documents://filename.jpg" といった記法を用いることで回避可能
  • またはフレームワーク自体を書き換えてしまう

■問題
以下のコードの様に、ファイルスキームのURLを用いて画像を読み込もうとするとエラーになります。
photos = [[NSMutableArray alloc] init];
PhotoBookPhoto *photo;
NSString *imagesDirectoryPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Images"];
NSArray *imagePathes = [[NSFileManager defaultManager] directoryContentsAtPath:imagesDirectoryPath];

for (int i=0; i<imagePathes.count; i++) {
NSString *imagePath = (NSString *)[imagePathes objectAtIndex:i];
imagePath = [imagesDirectoryPath stringByAppendingPathComponent:imagePath];
LOG(@"%d - %@ %@", i, imagePath, [[NSURL fileURLWithPath:imagePath] absoluteString]);
photo = [[[PhotoBookPhoto alloc] initWithURL:[[NSURL fileURLWithPath:imagePath] absoluteString]
smallURL:[[NSURL fileURLWithPath:imagePath] absoluteString]
size:CGSizeMake(960, 640)] autorelease];
photo.photoSource = self;
photo.index = i;
[photos addObject:photo];
}

■原因
TTRequestLoader 中の以下のコードが直接的な原因です。
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
#pragma mark NSURLConnectionDelegate


///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)connection:(NSHTTPURLConnection*)connection didReceiveResponse:(NSHTTPURLResponce*)response {
_response = [response retain];
NSDictionary* headers = [_response allHeaderFields];
本来 NSURLConnectionDelegate のデリゲートメソッドである、 connection:didReceiveResponse:NSURLResponce を引数にとるのですが、何故か NSHTTPURLResponce が直接指定されています。通常はこれでも問題ありませんが、fileスキームのURLは当然HTTPではありませんから、 NSHTTPURLResponse を返しません。従って allHeaderFields の呼び出しでエラーになってしまいます。


■対処法
ということで、この部分のコードを一部書き換えてしまい、 NSURLResponce がきても動作する様にしてしまいました。
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
#pragma mark NSURLConnectionDelegate


///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response {
_response = [response retain];
if ([_response isMemberOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)_response;
NSDictionary* headers = [httpResponse allHeaderFields];
int contentLength = [[headers objectForKey:@"Content-Length"] intValue];

// If you hit this assertion it's because a massive file is about to be downloaded.
// If you're sure you want to do this, add the following line to your app delegate startup
// method. Setting the max content length to zero allows anything to go through. If you just
// want to raise the limit, set it to any positive byte size.
// [[TTURLRequestQueue mainQueue] setMaxContentLength:0]
TTDASSERT(0 == _queue.maxContentLength || contentLength <=_queue.maxContentLength);

if (contentLength > _queue.maxContentLength && _queue.maxContentLength) {
TTDCONDITIONLOG(TTDFLAG_URLREQUEST, @"MAX CONTENT LENGTH EXCEEDED (%d) %@",
contentLength, _urlPath);
[self cancel];
}

_responseData = [[NSMutableData alloc] initWithCapacity:contentLength];
} else {
_responseData = [[NSMutableData alloc] initWithCapacity:response.expectedContentLength];
}
}
また、あとから知ったのですが、Three20においては以下のようなURLが使用できるみたいです。 Three20/TTGlobalCorePaths.h にて定義されています。
photo = [[[PhotoBookPhoto alloc] initWithURL:@"bundle://image.png"
smallURL:@"documents://thumbnails/smallImage.png"
size:CGSizeMake(960, 640)] autorelease];
たぶん Three20以外では使用できない記法 なので注意が必要ですが、この方法で読み込むと NSHTTPURLResponce が返却されるためコードを書き換えなくても正常に動作します。 こちらのほうが簡単でThree20の開発チームの人もそうしているのでお勧めです。


■んーでも
そもそもこのThree20、開発者の人(FaceBookアプリを作った人)がiPhoneでのプログラミングを既にやめちゃっていると聞いたのでメンテがどうなるのか不安です。

iPadアプリに挑戦中

運良くiPadを輸入して手に入れることができましたので、現在iPadアプリの作成にとりかかっています。最初はiPhoneと対して変わりあるまいと思って作っていたのですが、実機で動かしてみると様々な違いや問題が分かってきました。


■今作っている物とか課題とか

現在作っているのはiPad用の紙のノートです。



数年前からシステム手帳を持ち歩いていたのですが、実際スケジューリング等はすべてiPhoneで行っていました。それでも紙を手放せなかった唯一の理由がアイディア出しです。アナログ人間な物で、手で紙に書き付けないとアイディアが出てこないのです。iPhoneのスクリーンは明らかに小さすぎて手書きには不向きでした。

そこでiPadの大きくなった液晶を使えば紙の代わりができるのではないかと思って早速試して見ました。

ペンの色を4色、太さを4種類用意しています。二本指で左右にスワイプするとページをめくることができます。デバイスを横向きにすると、二ページ見開きの状態にすることができます。この見開きのままでも線を書いたりページをめくったりできます。



できるかぎり実際の紙に近づけたかったため、UIを一切排除しています。


■Gesture Recognizer

ページング処理やペン選択ツールの表示にはスワイプジェスチャを使用していますが、今回このジェスチャを実装するためiPhone OS 3.2より新しく搭載された UIGestureRecognizer というクラスを使用してみました。このクラスを使えば各種ジェスチャを自動的に認識してくれるので、自分でいちいちtapの位置を拾って前回のtapとの差分を検出し・・・ということをしなくても済むようになります。

// Add gesture recognizer to the paper view
UISwipeGestureRecognizer *toolPopoverGestureRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(handleToolPopover:)] autorelease];
toolPopoverGestureRecognizer.numberOfTouchesRequired = 2;
toolPopoverGestureRecognizer.direction = UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionUp;
[self.bookView addGestureRecognizer:toolPopoverGestureRecognizer];
しかしながらいくつか問題が。まず認識精度がそれほどよくないです。特に複数指のスワイプに関しては自分で実装した方が精度が出ると思います。また、ジェスチャは通常のタップとは別に検出されているようなので、二本指でタップしてジェスチャしたときには通常の線描画を切るような処理を含めないと、ジェスチャするたびに線が画面に増えてイライラします。


■他のお絵かきアプリのUI

予想通りというか、ふたを開けてみれば他にもたくさんのドロー系アプリがApp Storeにリリースされていたので、それらのUIを見て研究してみる事にしました。

Adobe ideas for iPad



二本指ドラッグでスクロール、ピンチ操作で拡大縮小。一本指で描画中に二本目が触れると即座に描画をキャンセルしてくれるため、誤ってキャンバスに線が増えてしまうということはありません。よく考えられています。

常に左横にツールバーが表示されているようになっています。ツールバーを置くのは邪魔だろうと思っていたのですが、キャンバスの拡大縮小や移動が自由にできればほとんど邪魔にはならないことがわかりました。むしろすぐアクセスできて便利です。

Autodesk SketchBook Pro



こちらも二本指ドラッグでスクロール、ピンチ操作で拡大縮小。一本指で描画中に二本目が触れると即座に描画をキャンセルしてくれるところも全く同じです。

ツールバーを表示するには、画面中央下の小さなポッチをタップするか、または指三本で画面にタップ。この方が画面を広く使えて嬉しい・・・と思っていたのですが、実際に試してみると意外とイライラします。三本指というのが直感的ではないのかもしれません。このへんは人によるのかもしれませんが、私は常にツールバーが表示されている方がスムーズに操作できました。

Penultimate



iWorkと非常に良く似た作りになっていて、しかも操作系はシンプルです。ジェスチャは一切なく、ボタンはツールバーとして常に表示。それはまったく問題ないのですが、画面がスクロールできず拡大縮小もないため、非常に画面が狭く感じ書きづらいです。いくらiPadが大画面とはいえ、ペン先が通常のペンに比べて太い(私が好んで使うペンは0.4mmですが、iPad上の指は10mmぐらい幅があるので、およそ25倍も大きい)ため、せいぜい数文字しか綺麗に書けません。

その他特筆として、ペンの書き味が素晴らしいです。描画速度が速く非常に追従性がよい。すらすら書けます。


■お絵かきアプリのUIまとめ

いくつか使ってみて、さらには自分でも作ってみて感じたのが以下のようなこと。
  • 紙ではなくてホワイトボードのメタファとして使用するとうまくいく
  • 拡大・縮小・スクロールは絶対必須 無いとアプリとして成立しない
  • ジェスチャを使ってメニューを出すのは思ったよりも効果的ではない
  • 書き味はや動作速度は大事、使っていて楽しくなる
まず一番目。ペン先が太く、消しゴム削除がたやすく、アンドゥリドゥもできるので、書いている間隔がホワイトボードに近い気がします。そのため紙ではなくてホワイトボードだと思って実装するとよさそうです。

二番目。ペン先がやたら太いわりには画面サイズが1024x768しかないので、拡大縮小スクロールできないと話になりません。逆にこれができれば事実上キャンバスサイズは無限大にできるわけで、デバイスサイズ以上の活躍をしてくれます。

三番目。ジェスチャ自体は上手く使えば非常に有効です。たとえばアンドゥリドゥなどの操作はジェスチャで行う方が直感的でした。しかしメニューは常時画面に表示していた方が良い気がします。

最後、四番目。動作速度は極めて大事だと感じました。特に描画速度は最も大事で、線を引くのがもっさりしてしまうとそのせいで綺麗な曲線にならなかったり、単純にイライラしたりします。実物のホワイトボードに書くぐらいの速さで描画ができるように目指したいです。

2010年4月1日木曜日

UIBarButtonItem の見た目を画像にしたいときのテクニック

iPhoneアプリでよく使われる、ナビゲーションバーに配置するボタン UIBarButtonItem の見た目を完全に画像にする時のテクニックです。
参考にしたページはこちら。
http://discussions.apple.com/thread.jspa?threadID=1505647
http://www.iphonedevsdk.com/forum/iphone-sdk-development/13809-uibarbuttonitem-customview-action.html
http://discussions.apple.com/thread.jspa?threadID=1546506&tstart=60


■作戦1:initWithImage
てか、 UIBarButtonItem には initWithImage あるからそれでいいんじゃないか、と思ってさっそく以下のコードを書いてみました。
UIBarButtonItem* buttonItem = [[UIBarButtonItem alloc]
initWithImage:[UIImage imageNamed:@"appstore.png"]
style:UIBarButtonItemStyleBordered
target:self
action:@selector(appstoreAction)];
self.navigationItem.rightBarButtonItem = buttonItem;
するとあら不思議、確かに画像は表示されたのですが、画像の周りにいつものボタンの枠が表示されてしまっています。styleを変更してもうまくいきません。どうやらこれはあくまでいつものボタンの中に画像を表示するだけのメソッドで、ボタンの見た目そのものをそっくりそのまま画像に入れ替える(ボタン画像を作ってさしかえる)用途には使えないようです。


■作戦2:initWithCustomViewとUIImageViewを組み合わせる
それなら initWithCustomView を使って画像を表示させてみましょう。
UIImageView* customView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"appstore.png"]];
customView.userInteractionEnabled = YES;
UIBarButtonItem* buttonItem = [[UIBarButtonItem alloc] initWithCustomView:customView];
buttonItem.target = self;
buttonItem.action = @selector(appstoreAction);
self.navigationItem.rightBarButtonItem = buttonItem;
この方法だと、ボタンの縁が消えてappstore.pngがそのまま表示され、見た目は臨み通りの状態に表示させることができたのですが、今度は別の問題が。なんとボタンを押してもアクションが呼びだされません。海外のフォーラムでも同じような問題を抱えている投稿が見受けられました。


■作戦3:initWithCustomViewとUIButtonを組み合わせる
それならということで、カスタムビューを UIImageView から UIButton に変更してみることにしました。
UIButton *customView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 95, 33)];
[customView setBackgroundImage:[UIImage imageNamed:@"appstore.png"]
forState:UIControlStateNormal];
[customView addTarget:self action:@selector(appstoreAction) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem* buttonItem = [[UIBarButtonItem alloc] initWithCustomView:customView];
self.navigationItem.rightBarButtonItem = buttonItem;
ビルドして試してみると、見た目は作戦2と同様に綺麗に表示され、さらにタップするときちんとアクションが実行されるようになりました!

2010年3月31日水曜日

Flash の埋め込みパラメータ wmode で発生したトラブルいろいろ

Flashまわりの仕事をしていて wmode パラメータで躓いたのでメモ。


■事の発端
開発機のMac上では正常に動作していたコンテンツが、Windows機のFirefoxでテストを開始したとたん以下のような症状に見舞われてしまいました。
  • TextFieldにフォーカスを合わせて日本語をタイプするとTextFieldとは全然違うとんちんかんな位置に入力中の文字列が表示される
  • しかも入力した日本語が文字化けする
ちょっと調べてみて、どうやら wmode パラメータが悪さをしているのではないかということが分かりました。
http://www.326studio.net/blog/2009/10/firefox.html
http://fukata.org/2009/11/29/1905/

ということで早速 wmode パラメータを消してみたところ上手く動作しました。


■そのときのSkype上での会話(一部改変)
登場する皆様ご紹介
 akisute: そもそもテキストボックス内に入力が表示されないのが普通な気がしてきた
easy: あいうえお=0B0D0F0H
akisute: http://www.326studio.net/blog/2009/10/firefox.html
akisute: こいつだな
wozozo: なんでその指定してたんだっけ
AE35: > htmlにswfファイルを呼び出すときにwmodeの指定でtransparent,opaqueの指定をするとこの現象が起きます。
akisute: http://fukata.org/2009/11/29/1905/
akisute: ということでをぞぞんよろしくお願いします
wozozo:
{
id : 'externalswf',
quality : 'high',
wmode : 'transparent',
allowScriptAccess : 'always',
width: 100,
height: 100,
flashvars: 'abesi hidebu tawaba'
});
wozozo: いまこうなってます。
wozozo: wmodeごと削除?
feiz: お、そこは以前のプロジェクトでぶつかった問題ですね
feiz: wmode=transparentを付けると
feiz: flash上にhtmlのダイアログを表示したりしたときにflashが上に出てしまう問題が回避できます
feiz: ただし日本語が化けます。
wozozo: e
wozozo: じゃあこれ消していいの
feiz: 両方いっぺんに回避する方法はあったっけ・・・
wozozo: でもいま
feiz: ああ、無いんだった
wozozo: flashの上にhtmlを表示するときには、
wozozo: embedされてるdivごと hide してる
feiz: あ、それが正しいはず
feiz: 以前のプロジェクトでもそうした。
wozozo: naru
wozozo: じゃあ消す。
feiz: ちなみに二つの問題は両方とも特定ブラウザ限がついてたはず。
feiz: どうでもいいけど。
akisute: それはすごい面白いネタなので
akisute: メモってBlogる


■さらに調査

調べれば調べるほどこの wmode には問題だのバグだのが山盛りだと言うことが分かって驚愕。
http://blog.graffiti-web.org/archives/2006/04/wmode.html
http://chabudai.org/blog/?p=34
http://d.hatena.ne.jp/jedisystemer/20090227/1235754536
http://3ping.org/2004/05/15/1441
Flash Player 10では修正されているらしいのですが、上記のバグはFlash Player 10で発生してますし、うーん。これは触らぬ神にたたり無しですね。


■結論
wmode は指定しないようにしましょう、と。Flash Player 9以下ではIEでフレームレートが落ちる問題がありますが、そもそも9を使っている人も少なくなりましたし問題ないと思います。

2010年3月20日土曜日

IE6, IE7 上で flash.net.URLLoader を使って Comet (Long Poling) によるプッシュ通知を行う際の注意点

Flashは一度書いたらどのブラウザでも同じように動く、そう信じていた時が私にもありました。その私の考えを裏切るかのように、 またIEか と言いたくなる現象が発見されました。

調査の際に参考にしたページはこちら。
http://saruzaurus.blogspot.com/2008/07/comet_10.html


■問題
IE6, IE7 で、 flash.net.URLLoader / flash.net.Loader を使用すると、同時に2本しかコネクションが張れません。通常の使い道でしたらこの制限で何ら問題はないのですが、問題になるのは CometTornado のように所謂ロングポーリングと呼ばれるHTTP通信の仕方をするアプリケーションを書いたときです。ロングポーリングではいったんHTTPリクエストをサーバーに投げた後、サーバーで何かイベントが発生するまでそのレスポンスをひたすら待ちます。従って常時HTTPコネクションが1本消費されている状態になるため、事実上1本しかHTTPコネクションを使うことができません。

またIE6, IE7 では、 flash.net.URLLoader.close() の実装にも問題があるようです。
http://www.kirupa.com/forum/showthread.php?t=335691
http://stackoverflow.com/questions/455656/urlloader-gets-stuck-when-polling
きちんと使用後に URLLoaderclose しているのですが、それでも何故かメモリ使用量が減らなかったり、コネクションが閉じられなかったりして貴重なコネクション数が消費されっぱなしになったりするなど、どうにもこうにも不安定です。



これらが原因となって、常時ロングポーリングでイベントを待ち、イベントを受け取ったら処理を行ってまたすぐロングポーリングを張りに行くようなアプリケーションを書くと、その他の通信を行った際にロングポーリングが止まってしまったりその他の通信がコネクション数限界で実行されなかったりします。


■原因
Flashは内部的にはブラウザの機能を用いて実装されています。したがってブラウザ側のHTTP接続数が2本であれば自動的にFlash側のHTTP接続限界も2本になります。当たり前と言えば当たり前なのですが、盲点でした。

Flash側の接続数とブラウザ側の接続数が共有されるかどうかは未知数・未検証ですが、おそらく共有されるのではないかと思っています。従って(まずこんな構成にすることはないと思いますが)FlashでCometによる通知待ちを行い、Ajaxでも同様の通知待ちを行うと、それだけで接続限界になってしまいます。


■対策
今のところ最良の方法がまだ分かっていません・・・ごめんなさいorz 一応いくつか思いついている作戦は、
  1. Windowsのレジストリを操作してIE6/IE7の同時接続数を増やす
  2. 同時接続数制限は同一ドメインに対してのみ発生するらしいので、サブドメインを切って別々のドメインに分ける
  3. 他の通信が発生しているときはロングポーリングを切断する
などなど。上手い対策が発見されましたらまた書きます。

2010/03/31 23:59 追記:サブドメインを切って別々のドメインに分けたら上手く動作しましたので、この方法を一応お勧めしておきます。

2010年3月18日木曜日

Flash の fl.controls / fl.containers の visible プロパティはADDED_TO_STAGEで変更する

最近イマイチなFlashネタしかお届けできておらず残念です>< が、ちょっとしたネタを発見しましたので書いてみます。


■問題
fl.controls.Buttonとか, fl.containers.ScrollPaneのvisibleプロパティをコンストラクタの中でfalseにしても、直後にtrueに戻されてしまうようです。試しに、以下のような構成のMovieClipをFlashで作成して、
  • TestMovieClip
    • aButton
    • aScrollPane
以下のようなコードを書いてみます。
package {
import flash.display.*;
import fl.controls.*;
import fl.containers.*;
public class TestMovieClip extends MovieClip () {
public function TestMovieClip() {
aButton.visible = false;
aScrollPane.visible = false;
}
}
}
このコードを実行すると、なぜかボタンとスクロールペーンが 消えません 。なんでやねん。デバッガを使って調べてみると、どうやらコンストラクタが終わった後、どこかのタイミング?でvisible=trueに戻っているようです。


■対策
以下のようにADDED_TO_STAGEイベントを使って回避します。
package {
import flash.display.*;
import flash.events.*;
import fl.controls.*;
import fl.containers.*;
public class TestMovieClip extends MovieClip () {
public function TestMovieClip() {
addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
}
private function onAddedToStage(event:Event):void {
aButton.visible = false;
aScrollPane.visible = false;
}
}
}
公式のドキュメントを見てもこのあたりの解説が無くて困りました。

2010年2月28日日曜日

Sphinx の Blogger 原稿用拡張を作ってみました

Bloggerの原稿をいちいち手で書くのが面倒だったので(特にリストとかpreとか)、Sphinxを使ってBloggerの原稿を書く拡張を作ってみることにしました。
参考にしたページはこちら。
http://sphinx.shibu.jp/


■サンプルとダウンロード

githubにアップしてみましたので、こちらからご利用いただけます。
http://github.com/akisute/sphinx_rst2blogger

実際に変換させてみたサンプルをこちらに用意してみました。
http://gist.github.com/317478


■設定

完全に私の個人使用向けになっているので、そのままではたぶん使いにくいと思います。まずはMakefileの以下の部分をご自身の環境に合わせて書き換えてください。
blogger:
$(SPHINXBUILD) -b blogger $(ALLSPHINXOPTS) $(BUILDDIR)/blogger
mate $(BUILDDIR)/blogger/draft.txt
@echo
@echo "Build finished. The draft pages are in $(BUILDDIR)/blogger."
今私は mate コマンドを使って結果をTextMateに表示するようにしていますが、これはTextMateがないと使えないので適当に消すなり pbcopy に書き直すなりすると幸せになれます。

また、Blogger側の以下の設定を有効にしてください。



■実際に使ってみる

使用方法はとっても簡単です。以下のコマンドをシェルから実行してください。
make blogger
make.batは全く操作していないのでWindowsの人はごめんなさい。たぶん使えません><

また、100%完璧な変換はできておらず、主にpreとかtitleの箇所で手動微調整が必要になります。まぁおおよそ上手くやってくれればいいや、という考えです。


■おまけ:Sphinxの拡張機能でカスタムのビルダーを作る方法

拡張機能の作り方自体は以下のページのやり方に沿えば楽勝です。
http://sphinx.shibu.jp/extensions.html
ですが、肝心のビルダーを作るのはかなり苦労しました。

ビルダーの作り方ですが、一から手で書くと挫折するので、もともとSphinxに付いているビルダーをまるごとコピーしてそれを修正するようにすると比較的楽でした。リファレンスとかドキュメントとかないので、ソース見て、 visit_hogehoge とか depart_hogehoge とか書いてあるところを適当に修正してみたら動いた、って感じです。 TextBuilder を元にすると書きやすかったです。 HTMLBuilder はdocutilの機能を拡張しているためdocutilがわからないとさっぱり分かりません。

Sphinx を用いて、 reST の文章を Blogger の原稿に変換する

改良版@2010/02/28 18:08

BPStudy #30 にてSphinx講座があったので、早速それを生かしてreSTでBlogger向けの原稿を作ってみることにします。


■見出しのテスト

Hello world!
foo bar
改行のテスト

段落のテスト

それではインラインマークアップのテストをしてみましょう。 強調強い強調code sample(): print sample 。いかがかな?

リンクのテスト リンクの末尾には必ず_を付ける必要があります。


■さらにもう一つ見出し
  • ul 1
  • ul 2
  • ul 3
    ul 3 continues
  1. ol 1
    1. ol 1-1
    2. ol 1-2
  2. ol 2
    ol 2 continues
    ol 2 continues again
  3. ol 3
番号リストの後に普通のパラグラフを入れてみるとどうなるか
定義リスト
定義リストをつくるにはインデントを頭に付けるなんか定義リストの作り方がおかしいって怒られた
どうすればいい?
しらんがな
どうするもこうするも
/** This is the pre message*/
def pre(self, message):
print 'this is the pre message'
だいたいうまくいっているようです。 okay fine.

Razor Orochi を Mac 用の無線ゲーミングマウスとして使ってみる



自宅のマウスを無線化したくなったので、Macでも左利きでも使える無線マウスを探してみたところ、 Razor Orochi という製品が見つかったので試してみることにしました。


■諸君 私はマウスが好きだ
「そもそもMacで無線マウスならMagic Mouseを使っておけばいいだろう」と皆様お考えであろうと思うのですが、残念ながら以下のような理由により私はMagic Mouseに満足できません。
  • Boot CampしたWindowsでの動作がイマイチ。基本的にMac専用。
  • ボタンがないためゲーミングで使用できない。右クリックすら怪しい。
  • 薄すぎ小さすぎて手に収まらない。
仕方がないのでその他の無線マウス製品を探してみたのですが、なんと以下の条件にマッチするマウスが世界中探しても一つもないのです!
  • 左利きなので、左右対称デザインであること
  • ゲーミングに使用するため、最低でも5ボタン付いていること
  • ゲーミングに使用するため、ホイールはクリック感があること(武器切り替えの時にクリック感のないホイールはまるで使えません)
  • 可能な限り大きく手のひらに収まるサイズであること、モバイルサイズはダメ
特に条件が厳しいのが「左右対称デザインであること」で、まぁ何処の会社も無線マウスは右手で持つものだと言わんばかりに大きく左にひねくれさせ放題ひねくれさせてくれやがりまして、まるで選択肢がないのです。あきらめかけていたときにようやく海外のサイトでこのRazor Orochiを見つけました。


■Razor Orochiって?
ゲーミング用製品で有名なRazor社の製作した世界初のBluetoothモバイルゲーミングマウスです。詳細はこちらのページをご覧になってください。

どうも日本国内では正式に流通していないようで、そもそも日本語版のRazor社の公式ページには名前すらありません。Razor社の通販ページから直接通販して買うしかありませんでした。送料込みで100ドルぐらいです。

おそらく日本国内でこんなへんぴなマウスをわざわざMacで使っているのは私ぐらいなものでしょう><


■設定してみる
最初に重要なことですが、Macだけで使用したいと思っていても、Razor Orochiの設定にはWindowsが絶対必須です。Mac版のドライバも用意されていますが、残念ながらMac版のドライバはOrochi本体のメモリに設定を書き込むことができず、またボタン配置を変更したりもできないため、全く役に立ちません。極めつけに有線モードでなければ設定できず、有線から無線に切り替えると設定がすべてリセットされるため、タダのお荷物です。USB Overdriveを代わりに使うことを強くお勧めいたします。

また、Razor Orochi ファームウェアの更新にもWindowsが必須です。ファームウェアの更新ですが、これが大変重要で、ファームウェアを更新しないとこのマウスはまるで役に立ちません。論より証拠、以下の図を見てください。



これはRazor Orochi farmware version 1.02の状態でお絵かきソフトで線を引いてみた図なのですが、見ての通り赤い線の部分がポインタ飛びを起こしています。高速で引いた線、水平垂直な線は比較的綺麗ですが、斜めにゆっくり動かすとすさまじい勢いでポインタが飛んで使い物になりません。farmware version 1.06にすると多少改善しますので、まず何はなくともファームウェアを更新する必要があります。

ということで、まずはWindowsを起動してファームウェアを更新し、それから設定を行います。私は左利きなので、サイドボタンの設定を変更する必要があります。初期設定では4,5が戻る/進むになっていますが、これを6,7と入れ替えてやります。こうすることで、左手の親指でマウスボタン4,5が押せるようになります。右利きの方はそのままの設定で十分使えると思います。


■使ってみる
良い点はきちんと5ボタン使える左右対称デザインで、しかもモバイルサイズながら十分な大きさがあり、手にフィットするところです。小指がマウスに乗っからないので若干小さいですが、この程度なら十分使えます。重さは結構ありますがすぐ慣れました。精度も、大きくマウスを振り回し続けている間は非常に良いです。Microsoft IntelliMouse Opticalとほとんど変わりません。

悪い点はなんと言ってもポインタが飛びます。このRazor 3G レーザーセンサー、低速時の挙動が最悪です。steelseries 4HDをマウスパッドにしているからわざと相性悪くしているのかと思って別のマウスパッドを使ってみたり直接机で動かしたりしてみましたが、やっぱりダメです。ファームウェア1.06で多少良くなりますが、スナイパーの方や細かい線を書くお仕事の方には絶対にお勧めしません。このセンサーの問題は有線モードにしても解決しません。

それからもう一つ悪い点として、スリープ状態からの起き上がりが非常に遅いです。5秒休めておいてから急にマウスを動かすと最初の1ストロークが完全に無視されます。最悪です。Magic Mouseは起き上がりが非常に早いのでそんなものなのかと思って買ってみましたが、なるほどアレはAppleのMac専用チートだったわけですねorz


■まとめ
まだ無線マウスは使い物にならないというのが正直な感想です。以前、ゲーミングデバイスを開発してらっしゃるSteelSeries社の方に無線マウスは作らないのかと質問してみたのですが、そのときに得た答えが「まだ現在の技術では無線マウスは使い物にならない」というものでした。なるほど納得です。

しかしながら全く使えないのかというとそうでもなくて、線が無くなったおかげで確かに机が広く使えて助かってます。私が最近プレイしているHeroes of NewerthのようなRTSゲームや、DiabloみたいなRPGスタイルのゲームであれば、常にマウスを大きく動かすためほとんど問題のない操作感になります。

逆にスナイパースタイルのFPSをプレイするのには全く向きません。日常使いにもちょっと不満があります。たいていキーボードを触っているので、ちょっとマウスをさわりに行くと遅くてイラッ。

2010年2月20日土曜日

hg clone で巨大なリポジトリを取得するときは --debug オプションが便利

参考にしたページはこちら。
http://mercurial.selenic.com/wiki/Clone

mercurial hgを使っていると、特にバイナリでサイズの大きいファイルを頻繁に変更するようなリポジトリ(たとえばFlashとかflaとかflaとか)はすぐにサイズが肥大化して、cloneに時間がかかるようになってしまいます。このような巨大リポジトリのcloneの進捗状況を表示できないかと思い調べてみました。

結論から言いますと、今のところそのようなコマンドは用意されていないようです。そこで代わりに--debugオプションをつけて実行するとよいと公式ページに説明がありました。
> hg clone --debug http://www.selenic.com/repo/hello my-hello
using http://www.selenic.com/repo/hello
sending between command
sending heads command
requesting all changes
sending capabilities command
capabilities: unbundle=HG10GZ,HG10BZ,HG10UN branchmap lookup changegroupsubset
sending changegroupsubset command
adding changesets
add changeset 0a04b987be5a
add changeset 82e55d328c8c
adding manifests
adding file changes
adding Makefile revisions
adding hello.c revisions
added 2 changesets with 2 changes to 2 files
updating the branch cache
updating to branch default
resolving manifests
overwrite False partial False
ancestor 000000000000 local 000000000000+ remote 82e55d328c8c
Makefile: remote created -> g
hello.c: remote created -> g
getting Makefile
getting hello.c
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
全体の進捗がわからないのでいまいちですが、何も表示されないよりは格段に良くなりました!

2010年2月15日月曜日

Flash の SWC は使ってはいけない

タイトルからしてなかなかひどいですが、調査結果もなかなかひどいです。

■結論:fl.controls(最初からFlashに付属しているコンポーネント)を含むSWCを作ると正しく読み込まれない
まずはこちらに検証用のプロジェクトを用意しましたので、ご覧になってみてください。
package {
import flash.display.MovieClip;

public dynamic class Abesi extends MovieClip
{
public function Abesi() {
trace("abesi");
trace(this.button, this.textArea);
}
}
}
package {
import flash.display.MovieClip;

public dynamic class Hidebu extends MovieClip
{
public function Hidebu() {
trace("hidebu");
trace(this.button, this.textArea);
}
}
}
とまぁ、なんの変哲もないタダのFlashコードをswcとして出力し、TestMainの中でnewして出力しているだけなのですが、なんとswcに書き出すタイミングに応じて確実にエラーになって落ちるというひどい問題があるようなのです。


■検証結果
以下のスプレッドシートにまとめてみました。
http://spreadsheets.google.com/ccc?key=0AoXhhCSOuqOtdE5rNy1wc2N2Z2JuV1NPUFBjdlRNeHc



たぶん私の書き出し方に何か問題があるのだろうとは思っているのですが、何処を見てもそれに関する情報が見あたらなかったので解決できず、結局swcの使用はあきらめることにしました><

2010年2月8日月曜日

ActionScript 3 の flash.net.Loader が読み込んだ MovieClip の挙動がローカルファイルとリモートファイルで変わるみたいです

flash.net.Loaderを用いて、リモートサーバーからswfファイルをロードし、画面に表示するようなアプリを書いていたのですが、そのときふとしたことから以下のような問題に気づきました。


■前提条件
仮に以下のような条件があるとします。
  • main.swfとresource.swfがあって、main.swfはflash.net.Loaderクラスを用いてresource.swfをロードしてくるものとする。
    var loader:Loader = new Loader();
    loader.load(new URLRequest("http://akisute.com/static/swf/sample.swf"); //リモートサーバーからロード
    loader.load(new URLRequest("../swf/sample.swf"); //ローカルファイルからロード
  • ロードしてきたswfは、以下のコードでloader.loaderInfo.contentから取り出して、MovieClipとして処理する。Loaderは破棄する。
    function onComplete(event:Event):void {
    var loaderInfo:LoaderInfo = event.target as LoaderInfo;
    var content:MovieClip = loaderInfo.content as MovieClip;
    var mc:MovieClip = content.getChildAt(0) as MovieClip;
    }
  • ロードしてきたswf(以下resourceと呼称)には合計200フレームのタイムラインがあり、1から100フレーム目までにleftというラベルがついて左向きのデータが、101から200フレーム目までにrightというラベルがついて右向きのデータが格納されている。1から100フレーム目までに右向きのデータは存在しない。その逆もしかり。


■問題
  • 対象のファイルresource.swfを、ファイルシステムからロードしたときは、なんの問題もなくラベルleftとrightを入れ替えることができる。
  • 対象のファイルresource.swfを、httpでリモートサーバーからロードしたときは、leftとrightを入れ替えた瞬間に、resourceのアニメーションがバグる。stop()やgotoAndPlay()などの命令を無視した動きをする。
  • ローカルファイルとリモートサーバー、どちらのresource.swfも完全に同一のファイル。ファイルサイズもタイムスタンプもsha1ハッシュも完全に一致。
・・・なにこれ。最初に見かけたときはAdobe焼き討ちじゃコラと思ったものです。


■調査:取ってきたswfをdescribeTypeしてみる
愚痴っても仕方がないので調査します。まずは取ってきたswfに対して、flash.util.describeType()を実行し、オブジェクトダンプしたXMLを見てみます。するとローカルファイルから取ってきた場合とリモートサーバーから取ってきた場合で、以下のような違い(diff)がある事が分かりました。
1c1
< ## LOCAL ##
---
> ## REMOTE ##
5,6c5
< <type name="sample_fla::left_33" base="flash.display::MovieClip" isDynamic="true" isFinal=&quot;false" isStatic="false">
< <extendsClass type="flash.display::MovieClip"/>
---
> <type name="flash.display::MovieClip" base="flash.display::Sprite" isDynamic="true" isFinal="false" isStatic="false">
15,18d13
< <variable name="leftArm" type="flash.display::MovieClip"/>
< <variable name="leftLeg" type="flash.display::MovieClip"/>
< <variable name="leftHead" type="flash.display::MovieClip"/>
< <variable name="leftBody" type="flash.display::MovieClip"/>
diffなのでちょっとわかりにくいかもしれませんが、よく見ると以下のような差異があることが分かります。
  • LOCALにはきちんとしたクラス名がついており、MovieClipのサブクラスになっている
  • LOCALにはプロパティ情報がきちんと残されている
  • REMOTEはタダのMovieClipクラスになっており、プロパティも一切無い
つまり、全く同一のファイルをロードしているにもかかわらず、flash.net.Loaderは、ローカルファイルからロードするかリモートサーバーからロードするかによって生成するMovieClipオブジェクトを変化させているということがわかりました。

どうしてこのようなことをになっているのか少し考えてみたのですが、おそらくセキュリティ要件の問題ではないかと言う結論に至りました。というのも見ての通り、ローカルファイルから読み込んだswfをdescribeTypeすると、クラス名としてファイル名とシンボル名がついてしまっていて、これを悪用することができるのではないかと危惧したのではと。

■対応:しかしさらに泥沼
クラス情報が欠落してMovieClipになってしまっているのが問題だとすれば、読み込まれる側のresource.swfのすべてのシンボルを「アクションスクリプトに書き出し」して、きちんとクラスとして定義すれば、問題が解決するのではないかと考えました。

ということで、早速試して見たところ・・・
TypeError: Error #1034: 強制型変換に失敗しました。flash.display::MovieClip@677b34c1
を rightArm に変換できません。

at flash.display::MovieClip/gotoAndPlay()
at com.akisute::Main/onLoadCompleted()
at flash.events::EventDispatcher/dispatchEventFunction()
at flash.events::EventDispatcher/dispatchEvent()
at flash.net::URLLoader/onComplete()
・・・bullshit。ジョブズがどうしてあんなにFlashを毛嫌いするのか分かった気がします(絶対違)。

まぁスタックトレースが出るようになってくれたので、なんとかなりそうです。さっきまではスタックトレースすら出さずに勝手にバグを出してくれやがってたので大変困っていました。まったく、これ読んで出直せ、Adobe。
Errors should never pass silently.


■考察:どうしてこうなるのか
さらに考えてみます。一見さっきのスタックトレースはロードに失敗してバグが発生している用に思われますが、実際にエラーを吐いているのはonLoadCompletedの中のgotoAndPlayです。そう、ロードが完了した瞬間にgotoAndPlay("right")を実行しているのですが、そこでエラーが発生しているのです。

ここでちょっと仮説を立ててみます。
  • ロードが完了された瞬間のresourceのcurrentFrameは1、つまりleftである。このとき、right側のパーツは一切読み込まれていない(nullである)。
  • gotoAndPlay("right")を実行すると、おそらくロードが完了しているMovieClip自身がタイムラインに従って101フレーム目以降の内部構造を補完しようとする。すなわち、right側のパーツ(MovieClipとかShapeとか)をどっからか取ってきてセットし、left側のパーツをnullにセットしようとする。ここまでの動きはデバッガを使って確認できました。
  • このとき、先ほどのflash.net.Loaderは完全な情報を持っていたのできちんとMovieClipを作ることができた(left側のパーツをセットできた)が、MovieClip自身にはその情報がないので、right側のパーツをセットしようとしてタダのMovieClipをrightArmクラスの変数にセットしようとし、キャストに失敗して落ちている。
・・・長くてすみません><
要するに、ローカルファイルからロードし生成されたMovieClipはデータを完全に保有しているのでMovieClip自身でnullフレームを埋めることができるが、リモートサーバーからロードしてきたswfから生成されたMovieClipは生成時にLoaderクラスによってデータを欠落させられているので、自力でnullフレームを埋めることができないのではないかと考えたのです。


■解決策:外部ロードするswfは、絶対に空のフレームを作るな
そこで以下のような対応をしてみました。
  • leftラベルの箇所にもright側のパーツを配置する。ただし見えては困るので、透明にして配置する。
  • 同様に、rightラベルの箇所にもleft側のパーツを配置する。
  • こうすることで、最初からすべてのフレームのデータがLoaderクラスによって読み込まれ、 nullフレームがないのでタイムラインがどのように動いても一度ロードされたデータが欠落することがない。従って問題なく動作する。
はたしてこの方法が功を奏し、見事に動作するようになりました!


■というのを
@moriyoshiさんから教えて貰いました。ありがとうございます!
・・・というか、なんでFlash使いでもないのにこんな詳しいんですか><

Mercurial と git の branch にまつわるちょっとした tips 3選

3選とか言ってますが大した内容ではございません>< すみませんすみません><


■1:gitのbranchは跡形もなく消せる
ほとんど常識ですが、以下のコマンドでgitのbranchは消せます。
git branch -D
このコマンドを実行すれば、たとえHEADに対してマージされていなくてもそのままブランチを消すことができます。ということで、ちょっとしたテストコードなどはブランチを切ってそこで実験し、後からブランチごとたたき落とす運用が可能です。

余談ですが、gitのbranchはSubversionと使い勝手や実装が似ている感じがします。Subversionのbranchもタダのディレクトリコピーなので、好き勝手に作って消してが可能ですから。


■2:Mercurialのbranchは基本消せない、「未使用」か「クローズ」にはできる
問題はここから。Mercurialのbranchは、基本的に完全に跡形もなく削除することができません。また、マージしていないブランチがあるとpushの際に怒られます。そのため、ちょっとしたテストコードをブランチ切ってそこで作成し、いざいらなくなったので削除しようと思うとえらい大変なことになります。

一応、ブランチを「クローズ」するコマンドを使うことで常用範囲内からは消す事が可能ですが、完全に情報が消えたわけではないのでリポジトリのサイズは小さくなりません。ゴミが溜まります。もしMercurialでテストコードを管理しようと思うならば、branchを切るのではなくリポジトリごとcloneしてそちらでテストする運用にすればよいです。


■3:Mercurialのbranchに数字で名前を付けてはならない
gitと違ってMercurialのリビジョンには、簡便のため、リビジョンのハッシュIDの他にリビジョン番号が1番から採番されるようになっています。で、各種操作の際に、このリビジョン番号を代わりに指定して実行することが可能です。

ここまではOK。

問題はbranchに数字の名前が付けられることで、もしbranchに数字の名前を付けてしまった場合、Mercurialはその数字をリビジョン番号として解釈してしまい、ブランチ名指定の処理ができなくなってしまいます。
hg branch 255
hg commit -m "created branch 255 for ticket #255"
hg update 255
# ブランチ255ではなくリビジョン番号255番に対してアップデートしようとする・・・
ただそれだけなのですが、チケットシステムなどと連携しているとよくチケット名でブランチを切る事があると思いますので、そのような際にはticket255などという風に指定するほうがよいかと思います。

2010年2月7日日曜日

Mercurial の、 hg revert / hg rollback / hg backout の使い分け

以前からgitを使っていたのですが、最近は職場のバージョン管理システムがMercurial hg になっているので、もっぱらhgばかり使っています。ということで、いくつか覚えたhgネタ。

Mercurialやgitに限らず、いかなるバージョン管理システムを使用していても、人間が使う以上運用中にミスが発生することは避けられません。今回はMercurial使用中に間違ったコミットやプッシュを行ってしまった際の対処法を調べてみました。

参考文献はこちら。
4798021741入門Mercurial Linux/Windows対応
秀和システム 2009-01

by G-Tools


間違いを修正するためのコマンドは、大きく分けて以下の3つがあります。
  • hg revert
  • hg rollback
  • hg backout
またコマンドを用いて修正する意外にも、ローカル作業する際に作業用リポジトリを別に作って、問題に気づいたらリポジトリごと削除するという運用もありますが、今回は説明を省きます。


■hg revert
適用可能なのは「ローカルのリポジトリ上で、コミットをする前に問題に気づいたとき」。
作業中にちょっと間違った際など、ファイル単位でコミット前の状態(parentの状態)に戻すことができ大変便利です。コミットしてしまった場合は、次のhg rollbackを使用します。


■hg rollback
適用可能なのは「ローカルのリポジトリ上で、コミットをした直後に問題に気づいたとき」。
直前の1回分だけ、commitを取り消すことができます。マージ作業だろうがなんだろうが跡形もなく消してくれるため、後から見ても痕跡がなく綺麗です。操作も簡単で安全なため、これが使用可能なときはこちらを使用すると便利です。

一応、直前の1回だけならリモートの共有リポジトリへのpushも取り消すことができるのですが、これは後述の理由からお勧めしません。一度リモートにpushしてしまったら、次のhg backoutを使って取り消すことになります。


■hg backout
適用可能なのは「ローカルのリポジトリ上で、複数回コミットした後にに問題に気づいたとき」か、または「リモートの共有リポジトリにプッシュしてしまった後に問題に気づいたとき」。
どうしようもなくなってしまったときの最後の手段がこれ。指定したリビジョンのcommitの内容を完全に打ち消す新しいリビジョンを作ってくれます。あとは、手動でこの自動で作られたリビジョンを元のリビジョンにmergeしてcommitすることで、いかなる問題であろうとも無理矢理打ち消す事が可能になります。

もっぱら間違ってプッシュしてしまった際に使用します。
一応hg rollbackでも直前のpushを取り消すことが可能ですが、一度pushされてしまった内容はどこかの誰かのリポジトリにpullされてしまっている可能性があり、そうなると共有リポジトリだけをrollbackしても効果がありません。(このように、間違いも分散して広まってしまうのが分散バージョン管理の恐ろしいところ)

このようなときは、hg backoutで修正リビジョンを作りマージしたあと、再度共有リポジトリにpushすれば、他の誰かのリポジトリに問題が取り込まれてしまっていても次のpullで無事修正されます。

2010年1月26日火曜日

Google Docs の Presentation がチャートを描くときに地味に便利らしい

@shibukawaさんに教えていただいたのですが、

http://twitter.com/shibukawa/status/8210913937

@akisutesama Google DocsのPresentationが地味に便利です。チャートを書く時には。


またまたぁ。Google DocsのPresentationなら昔一度使ったけど、最低限の図しか作れなかったですよ。と半信半疑でGoogle Docsを開いてみたら、



あらまぁこれは凄い。いつの間にこんなオプションが増えてるんですか。普通にPowerPoint 2003程度の図が描けてしまいました。
知らない間にどんどん進歩してますね、Google Docs。本当にそろそろOfficeいらないですね。

2010年1月9日土曜日

ASDoc の書き方講座

仕事用に作ったまとめをそのまま転載。


■参考リンク
以下のリンクを見ておけば一通り大丈夫です。
ASDoc コメントの作成
ASDoc タグ一覧
@see タグや@copy タグの使用方法
びんた先生(beinteractive)のパーフェクトASDocタグ講座


■Flex 3 SDKのasdocコマンドを使ってみる
asdoc -helpを見れば良いのですが、helpが見づらくてややっこしいので、まとめてみました。
http://livedocs.adobe.com/flex/3/html/help.html?content=asdoc_9.html#142061
以下のコマンドを入力すればOKです。ここで、$FLEX_HOMEは Flex SDKをインストールしたパスです。
asdoc -compiler.source-path=自分が追加したいソースパス, -compiler.library-path=$FLEX_HOME/frameworks/libs,自分が追加したいライブラリパス -doc-sources=ドキュメントに出力したいソースのパス
$FLEX_HOME/frameworks/libsをライブラリパスに含めないとビルドが通らないのがくせ者です。JavaDocはtools.jarを自動的にパスに含めてくれるんだから、ASDocだって自動的にやってくれと言う気がします>< ちなみに、Flex Builderとか使えばこんな面倒な事をしなくても簡単にASDocを出力することができるみたいです。


■気をつけるところ
ASDocの最初の一文は必ずピリオドで終了する
腹立たしいですがピリオド{{.}}が最初の一文の区切りになっているようなので、句読点{{。}}ではなくピリオドを使いましょう。 たとえば、
package {
/**
 * ASDocのテスト用のクラスです.
 * 
 * 第一文は必ずピリオドで終了する必要があります。
 * 試しに上記のピリオドを句読点に変更してドキュメントを出力すると違いが分かると思います。
 */
 public class ASDocTestClass {}
 }
のように書く必要があります。こうしないと、ドキュメントを生成したときに説明文が長ったらしくなって困ります。

パスワードが無くなってログインできない Mac に無理矢理ログインする方法

会社で前任者が使用していたMacを使用する際など、管理者ユーザーのパスワードが紛失してしまってログインできなくなってしまう場合があります。っていうか、実際になって大変困りました><
このような場合、Mac OS Xでは「シングルユーザーモード」でOSを起動して、ユーザーのパスワードを自由に書き換えてログインできるようにすることができます。万が一の場合に便利なのでご紹介いたします。

参考にしたページはこちら。
http://d.hatena.ne.jp/viz3/20091029


■対象環境
  • Mac OS X 10.4以上であること (launchctlを使用するため)
  • UNIX / Linuxの対話環境についてある程度以上の知識があること
  • 失敗しても泣かない心があること
大変申し訳ありませんが、こちらに記載されている内容を実行して失敗してシステムが起動しなくなっても当方は責任を負いかねます><


1.シングルユーザーモードでMacを起動する
Macの電源を押して起動する際に、キーボードのCommand + Sキーを同時に押しっぱなしにしておくと、シングルユーザーモードで起動することができます。真っ黒な画面に文字がずらずら出てきたら成功です。


2./を読み書き可能にする(再マウントする)
画面上に
If you want to make modifications to files:
/sbin/fsck -fy
/sbin/mount -uw /
と表示されているので、ここに表示されているコマンドをそのまま入力します。コマンドの内容は以下の通り:
  • /sbin/fsck -fyは強制的にディスクチェックとディスクの問題修正を行う。-yがついているので、全ての問い合わせに対してYESを返したものと見なす。
  • /sbin/mount -uw /はすでに読み取り専用モードでマウントされている/を再度書き込み可能なモードでマウントし直す。

3.dsclをlocalonlyモードで起動する
お次はdsclというツールを使用します。ですがこのツールそのままでは起動できないようなので(dscl .とタイプするとなにやら警告が出ると思います)、以下のようにしてlaunchctlを使ってdsclツールの設定ファイルを読み込んでから、dsclをlocalonlyモードで起動します。
launchctl load /System/Library/LaunchDaemons/com.apple.DirectoryServicesLocal.plist
dscl localonly


4.対象のアカウントのパスワードを書き換える
dsclがlocalonly対話モードで起動したら、cdコマンドとlsコマンドを利用して対象のアカウントのある/Local/Target/Usersまで移動します。
 > ls
Local

Contact
Search
> cd Local
/Local > ls
Default
Target
/Local > cd Target
/Local/Target > cd Users
/Local/Target/Users > ls
ずらずらとここにアカウント名が羅列されるはず
ここまで移動できたら、以下のコマンドを入力すればパスワードが書き換わります。
passwd 対象のユーザー 新しいパスワード
たとえばさっきの状態からユーザーakisuteのパスワードをabesiに書き換えたいなら、
passwd akisute abesi
です。


5.再起動してログインする
あとは再起動して普通にMacを起動します。
reboot
先ほど入力したパスワードでログインできるはずです。


■注意
この方法を用いれば、物理的にアクセス可能な任意のMacの操作権限を奪うことが可能になってしまいます。くれぐれも悪用しないように。

逆に、この方法を用いてアクセスされたくないMacについては、以下のページで紹介されている方法を用いてシングルユーザーモードによる起動を禁止することができます。
Mac OS X 10.1 以降で、ファームウェアのパスワードプロテクションを設定する


■UNIX / Linuxユーザー向け
Mac OS Xのユーザーって、/etc/passwdじゃなくてDirectory Serviceで管理されてるんですねー。最初/etc/passwd以下を調べに行ってハマりました・・・

2010年1月3日日曜日

Google Code Prettify のテスト

■Python(自動判別)
abesi='abesi'
hidebu=3
print abesi+hidebu
def tawaba(n=0):
result = 0
for i in range(n):
result = result + i
return result
きちんと自動判別してくれているみたいです

■Python(言語指定)
abesi='abesi'
hidebu=3
print abesi+hidebu
def tawaba(n=0):
result = 0
for i in range(n):
result = result + i
return result
lang-pyを指定してみました

■Objective-C(自動判別)
NSString *abesi = @"abesi";
int hidebu = 3;
NSLog(@"%@%d", abesi, hidebu);
@synthesize anProperty;
#pragma mark -
#pragma mark Functions
int tawaba(int n) {
int result = 0;
for (int i=0; i<n; i++) {
result += i;
}
return result;
}
こちらも判別に成功。

■JavaScript(自動判別)
var abesi = 'abesi';
var hidebu = 3;
console.log("%s%d", abesi, hidebu);
// 日本語コメントのテスト
/**
* 日本語コメントのテスト
*/
function tawaba(n) {
var result = 0;
for (var i=0; i<n; i++) {
result += i;
}
return result;
}
日本語コメントのテストなど。

いまいち配色が気に入りませんが、svnのtrunkに直リンクしていてこちらでは修正できないのでまぁ仕方ないかなと思います。それからActionScript3に未対応なのがかなり不満です。外部CSSとJavaScriptが増えて重くなるしonloadのタイミングで実行しなくちゃいけないし・・・Googleらしからぬイマイチさ。