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では保存しないとか、一時エンティティにするとか。