ラベル iPhone の投稿を表示しています。 すべての投稿を表示
ラベル iPhone の投稿を表示しています。 すべての投稿を表示

2010年12月29日水曜日

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

ASIHTTPRequest という神の通信ライブラリを使って、うまい具合に外部 API から非同期的に結果を取得・解析して返すようなクラスをつくってみました。以前 NSOperation でやってみたバージョンは こちら。


■主な機能

元々の ASIHTTPRequest にある機能はもちろんご利用いただけます。多すぎて説明し切れませんので、以下の記事を参考にしていただければと思います><

http://d.hatena.ne.jp/ninjinkun/20101122/1290394265
http://macisv.jp/blog/?p=235

さらに今回私が作成した ASIAPIRequest にはこのようなおいしい特典がつきました。
  • POST のパラメータだけではなく、 GET のパラメータも楽々生成してくれるメソッドを用意しました。
  • 非同期実行時の通知方法が, delegate, blockに加え、さらに NSNotification による通知もサポートしました。
  • 非同期的に取得したレスポンスの値をパースするためのコールバックを用意しました。このコールバックメソッドをオーバーライドして、サブクラスで処理を行えば、この中の処理はすべて非同期実行されるため、 XML のパースが遅くて UI が固まったなんてことはもうありません。
  • おまけ的にタグとかつけられるようにしてみました。
その他、お使いになられる際に適当に ASIAPIRequest の中身を書き換え御社のプロジェクトに合うように調整するなどすると面白いと思います。自動的にログインパラメータをつけるようにしたりとか。


■ダウンロード

github にリポジトリを作りましたので、こちらから git でクローンするか、または master のソースコードをダウンロードしてください。

https://github.com/akisute/asi-http-request

タグが付与されていますが、これはクローン元のタグなので、当てにしないでください。常に master の先端をダウンロードするのが一番確実です。

ダウンロードしたら、 Classes ディレクトリと External ディレクトリの中身を適当に自分のプロジェクトにコピーして、プロジェクトに追加していただければOKです。最後に、以下のフレームワークをリンクしてください。
  • CFNetwork
  • SystemConfiguration
  • MobileCoreServices
  • CoreGraphics
  • zlib
ライセンスは元のライブラリに合わせて BSD ライセンスとします。


■使い方

最初に ASIAPIRequest を継承してサブクラスを作成します。
// APIAuthorize.h

@interface APIAuthorize : ASIAPIRequest {
}

// 認証APIのインスタンスを生成する
+ (id)apiWithUserId:(NSString :)userId password:(NSString :)password;

@end
では次にAPIインスタンスを生成するためのクラスメソッドをサブクラスの内部に作ってみましょう。
// APIAuthorize.m

+ (id)apiWithUserId:(NSString :)userId password:(NSString :)password {
NSURL *url = [NSURL URLWithString:@"authorize.json" relativeToURL:API_BASE_URL_STRING];
APIAuthorize *api = [APIAuthorize requestWithURL:url];
api.requestMethod = @"POST";
[api setPostValue:userId forKey:@"userId"];
[api setPostValue:password forKey:@"password"];
api.postRequestFinishedNotificationName = @"APIAuthorizeDidFinishNotification"; // POST 成功時に飛ぶnotificationの名前
api.postRequestFailedNotificationName = @"APIAuthorizeDidFailNotification"; // POST 失敗時に飛ぶnotificationの名前
return api;
}
最後にサブクラス内部でスーパークラスのメソッドをオーバーライドし、通信完了直後に呼び出される処理を記述します。たとえば、レスポンスが返ってきた際に、受け取ったレスポンスをパースして DB に保存したりします。このコールバック内部は UI スレッドとは別のスレッドで並列実行されているので、この中でどれだけ重い処理をしても UI は固まりません。その代わり UI を操作する処理はここでは行わないでください。クラッシュします。
// APIAuthorize.m

- (void)postRequestFinished {
// レスポンスステータスコードが異常系の場合はなにもしない
if (self.responseStatusCode != 200) {
return;
}

// レスポンスをパースしてオブジェクトにし、Core Dataに保存する
// 保存したオブジェクトをuserInfoに格納しておく
User *user = [User managedObjectFromJsonString:[self responseString]
inContext:[AppDelegate appDelegate].managedObjectContext];
[[AppDelegate appDelegate].managedObjectContext save:nil];
self.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
user, @"user",
nil];
}

- (void)postRequestFailedWithError:(NSError *)theError {
// なにもしない
}
tag プロパティを使って、同じAPIでレスポンスの種類を分けることができたりします。
// APIAuthorize.m

enum {
APIAuthorizeResponseTagUser,
APIAuthroizeResponseTagToken,
};

+ (id)apiWithUserId:(NSString :)userId password:(NSString :)password {
NSURL *url = [NSURL URLWithString:@"authorize.json" relativeToURL:API_BASE_URL_STRING];
APIAuthorize *api = [APIAuthorize requestWithURL:url];
api.requestMethod = @"POST";
[api setPostValue:userId forKey:@"userId"];
[api setPostValue:password forKey:@"password"];
api.postRequestFinishedNotificationName = @"APIAuthorizeDidFinishNotification"; // POST 成功時に飛ぶnotificationの名前
api.postRequestFailedNotificationName = @"APIAuthorizeDidFailNotification"; // POST 失敗時に飛ぶnotificationの名前
api.tag = APIAuthorizeResponseTagUser // このAPIのレスポンスはUser型だよーとタグをつけておく
return api;
}

- (void)postRequestFinished {
switch (self.tag) {
case APIAuthorizeResponseTagUser:
// Userオブジェクトを作る
break;
case APIAuthroizeResponseTagToken:
// Tokenオブジェクトを作る
break;
default:
break;
}
}
これで API 本体は完成したので、早速実行してみましょう。以下の4つの方法で実行が可能です。
  1. 同期実行
  2. 非同期実行、 delegate で結果を通知してもらう
  3. 非同期実行、 NSBlock で通信完了後の処理を行う
  4. 非同期実行、 NSNotification で結果を通知してもらう
1, 2, 3 については普通の ASIHTTPRequest と同じですので割愛します。 4 は私が新しく追加した機能で、 NSNotification の仕組みを使って実行完了通知を受け取ることが可能になります。たとえばこんな感じになります。
// 適当に認証とかする画面のViewController.m

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Notification通知を開始する
[[NSNotificationCenter defaultCenter] addObserver:self
name:@"APIAuthorizeDidFinishNotification" // さっき決めた文字列
target:self
action:@selector(apiAuthorizeDidFinish:)
object:nil];
}

- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Notification通知をオフにする
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (IBAction)startAuthorize {
ASIAPIRequest *api = [APIAuthorize apiWithUserId:self.userIdField.text password:self.passwordField.text];
[api startAsynchronomus];
}

- (void)apiAuthorizeDidFinish:(NSNotification *)notification {
// 認証完了時の処理
}
この通知はメインスレッドから呼び出されるので、自由に UI を操作することが可能です。

delegate と比べて、 NSNotification を使った通知の便利な点は以下の通り。
  • delegate を使う場合には、 delegate オブジェクトがメモリから消える前に delegate の始末を行う必要があるが、 NSNotification を使った場合にはその必要がない。自分を NSNotificationCenter から削除するだけでよいので、 API リクエストを比較的投げっぱなしにできる。
  • delegate とは違い、複数のオブジェクトで同時に通知を受け取ることができる。たとえば API を実行した画面とは全然違う別の画面二つで同時に通知を受け取ったりすることが可能になる。

■余談

この ASIHTTPRequest は本当にすばらしいです。最初にこの通信ライブラリを知ったときは、また良くあるただのちょっと便利なだけな通信ライブラリなんだろうと思い気にもとめなかったのですが、実際にコードを見てびっくりしました。私がほしかった通信ライブラリそのものだったからです。

私は通信ライブラリは NSOperation を継承して作るべきだと考えており、実際以前に試してみたことがありました。それは主に、
  • NSOperationQueue のシングルトンインスタンスが勝手に通信をすべて管理してくれるので、クラスの変数として通信クラスのインスタンスを保持しておかなくても良くなるかもしれない
  • NSOperation には依存関係を指定するメソッドがあるので、これを用いれば自動的に通信 A, B, C を順序通りに実行するなどできるかもしれない
  • NSOperation を継承すれば、将来 Apple の中の人がフレームワークを改善した際にマルチコア化した iPhone の CPU の恩恵を自動的に受けられるかもしれない
という考えがあったからです。そしたら見事にこの ASIHTTPRequest が NSOperation を継承して、しかも何かと問題の多い NSURLConnection を使わず NSStream とソケットを用いて自分で Run Loop を回すというすばらしい実装をしているじゃないですか!こりゃもうかないません。自分でやる必要が丸でなくなってしまいました。しかもそれだけではなく、
  • 現在全体の何%まで読み込みが完了したかを delegate で通知できる
  • 通信完了時に呼び出される delegate method はすべて main thread から呼び出されるので、 UI 操作をしても安全
という、まさにかゆいところに手が届く良さがすべてあります。惚れる。もうおそらく当分の間はこれ以上の通信ライブラリが現れることはないんじゃないかと言い切って良いぐらいすてきです。

アプリのビルド時に CSSMERR_TP_NOT_TRUSTED エラーが発生したときの対処法

http://d.hatena.ne.jp/drill256/20090820/1250752178
http://discussions.apple.com/thread.jspa?threadID=1630090

このエラーは、以下の証明書がすべて存在しないか、または Keychain Access 内でのステータスが、「この証明書は信頼されています。」ではないときに発生するようです。
  • Apple Worldwide Developer Relations Certification Authority
  • iPhone Developer または iPhone Distribution


上記の画像のように、「この証明書は信頼されています。」と緑色のチェックマーク付きで表示されている必要があります。そうでない場合は何らかの問題があります。

対処法は、
  1. まず何はなくともこれらの証明書がすべて存在するか確認する。 Apple Worldwide Developer Relations Certification Authority を忘れているケースが良くあります。
  2. 「この証明書は信頼されています。」になっていない場合には、証明書を選択して、「情報を見る」 -> 「信頼」 -> 「システムデフォルトを使う」 を選択する。「常に信頼する」ではダメです、エラーになります。
証明書をどのキーチェイン項目に入れていても問題はなさそうです。私の場合は WWDR をシステムに、 iPhone Developer をログインに入れていますが正常に動作しています。

2010年9月12日日曜日

iPhone 開発規約まとめ

あんまり iOS 上での開発規約とか見かけないので、試しに私が今個人/会社で使っている開発規約を公開してみることにしました。


■設計

設計は所謂 MVC と呼ばれる設計モデルを採用します。ただし、厳密な MVC というわけではなく、以下のような区分になっています。
  • Model
    Core Data を使用します。通常 MVC での Model というと業務ロジック等を含めた業務モデル一般すべてを含むのですが、私の場合は特に Core Data の NSManagedObject を Model として扱い、 Model 単体のみで完結するロジックのみを Model に記述します。たとえば、
    • Core Data から対象の Model とその関連 Model 取得
    • Model の新規作成
    • 新規作成時、更新時に自動的に Model のプロパティを更新する
    • Model のプロパティの値を元に幾何学計算をしたり、一時的に使うキャッシュを用意したりする
    などです。実際のコードのサンプル (ニュースサイトのニュースを表す News モデルの実装) はこんな感じになります:
    #import "News.h"

    @implementation News

    @dynamic body;
    @dynamic title;
    @dynamic imageURL;
    @dynamic objectId;
    @dynamic dateCreated;
    @dynamic dateUpdated;
    @dynamic sortOrder;

    @end

    //----------------------------------------------------------------------------------------
    // ここから上は .xcdatamodel ファイルから自動生成されたコードなので触れないようにします。
    //----------------------------------------------------------------------------------------

    #import "AppDelegate.h"

    @implementation News (NSManagedObject)
    - (void)awakeFromInsert {
    [super awakeFromInsert];
    // Insert
    self.dateCreated = [NSDate date];
    }
    - (void)willSave {
    [super willSave];
    // Update
    // DO NOT DO THIS in here because updating self.dateUpdated will call -willSave method recursively until this application crashes!
    // Use NSManagedObjectContextWillSaveNotification / NSManagedObjectContextObjectsDidChangeNotification and watch the time delta of previous change
    // If the time delta is small enough to ignore, do not update dateUpdated property to avoid infinite loop
    //self.dateUpdated = [NSDate date];
    }
    - (void)awakeFromFetch {
    [super awakeFromFetch];
    // Select
    }
    @end

    @implementation News (DBAccessors)
    + (NSArray *)all {
    AppDelegate *appDelegate = [AppDelegate appDelegate];
    NSFetchRequest *request = [appDelegate.managedObjectModel fetchRequestTemplateForName:@"allNews"];

    // Sort by sortOrder, ASC
    NSArray *sortDescriptors = [NSArray arrayWithObjects:
    [[[NSSortDescriptor alloc] initWithKey:@"sortOrder" ascending:YES] autorelease],
    nil];
    [request setSortDescriptors:sortDescriptors];

    NSError *error = nil;
    NSArray *resultArray = nil;
    if (!(resultArray = [appDelegate.managedObjectContext executeFetchRequest:request error:&error])) {
    // handle the error;
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    return nil;
    }
    return resultArray;
    }
    + (id)get:(NSString *)targetId {
    AppDelegate *appDelegate = [AppDelegate appDelegate];
    NSDictionary *substitutionVariables = [NSDictionary dictionaryWithObject:targetId
    forKey:@"objectId"];
    NSFetchRequest *request = [appDelegate.managedObjectModel fetchRequestFromTemplateWithName:@"getNews"
    substitutionVariables:substitutionVariables];

    NSError *error = nil;
    NSArray *resultArray = nil;
    if (!(resultArray = [appDelegate.managedObjectContext executeFetchRequest:request error:&error])) {
    // handle the error;
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    return nil;
    }
    return [resultArray lastObject];
    }
    + (void)deleteAll {
    AppDelegate *appDelegate = [AppDelegate appDelegate];
    NSFetchRequest *request = [appDelegate.managedObjectModel fetchRequestTemplateForName:@"allNews"];

    NSError *error = nil;
    NSArray *resultArray = nil;
    if (!(resultArray = [appDelegate.managedObjectContext executeFetchRequest:request error:&error])) {
    // handle the error;
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    return;
    }

    // Delete all objects fetched
    for (NSManagedObject *obj in resultArray) {
    [appDelegate.managedObjectContext deleteObject:obj];
    }
    }
    @end
  • Operation
    本来の MVC モデルには存在しませんが、私は特別に Operation という区分をもうけています。定義は以下の通り。
    • 一つの業務モデル、業務ロジックを完結させるひとまとまりのロジックであること。要するに本来の MVC モデルの業務ロジック的要素であること。 もっと言うなら Operation だけを切り出して単体テストできること。
    • それなりに重要でかつ量があり、 Controller や Model から共有的に呼びだされること。
    • 非同期で処理ができること。
    • NSOperation クラスのサブクラスにすること。
    たとえばインターネット経由で API を実行して結果を Model に突っ込む処理などは Operation として実装しています。 NSOperation として実装することで、非同期処理が簡単にできるようになるだけではなく、 タスクの依存関係を決めて先に親タスクが実行されるのを待つようにしたり、現在の状態を厳密に判定したり、タスクをキャンセルしたりするのが容易になります。また、将来的に iPhone / iPad のCPUがマルチコアになった場合、 NSOperation を継承しておけばほとんど何もしなくてもその恩恵を受けることができるはずです。
  • View
    ビューです。主に UIKit および Three20 を用いて構築し、必要に応じてその他のビューライブラリを組み込みます。個人的には UIView のサブクラス一般をすべて View として扱っています。 View には描画ロジックと幾何学計算などの補助ロジック、およびそれに必要な最小限のデータのみを持たせるようにします。状態を持ってもかまいませんが、状態のコントロールを View 自身が行うのはできる限り避けます。タッチイベントのハンドリングは必要に応じて View で touchesBegin: などを実装して行いますが、基本的には Controller 側で UIGestureRecognizer を用いて行います。
  • Controller
    コントローラーです。私の中での定義は UIViewController のサブクラスです。アプリの内容に応じて UIKit と Three20 を使い分けます。 本来 Controller は全体の流れのみを管理し実際の処理を行ってはならないということになっていますが、 UIViewController の性質を鑑みて、私は次のように Controller の役割を定義しています。
    • View の管理。
    • 画面遷移, 要するに Controller 間の遷移の管理。
    • タッチイベント、ジェスチャイベント、加速度計などからの入力の受付と処理。
    • 各種 UI コンポーネントの Delegate 処理。 UITextView, UIPopoverController, UIActionSheet, などなどなど多岐にわたります。
    • Operation 完了時の処理など、各種 Notification を受け付けて処理。
    • 上記に必要になるデータ/メソッドの定義。 View に持たせるぐらいならこちらに持たせる。他に持たせるところがないなら Controller にすべて持たせる。
    ということで、 Controller を名乗っていますが実際にはかなり自分で処理をします。こんなのいちいち分けていたら大変ですし・・・といういいわけです。実際のコードサンプルはお見せできないのですが、実装の概要はこんな感じです



  • Application Delegate
    これも本来の MVC モデルには存在せず、本来は Controller として扱うべき物だと思いますが、私は特別に分けています。 Application Delegate は所謂アプリケーショングローバルなデータや設定、オブジェクト、ユーティリティメソッドを管理するものとして扱います。たとえば、
    • Operation 駆動用の NSOperationQueue
    • Model のための NSManagedObjectContext / NSManagedObjectModel / NSPersistentCoordinator
    • 現在通信可能か否かを判定するためのユーティリティメソッド
    • アプリケーショングローバルな Notification を受け取って Application Delegate のプロパティとしてセットする
    などです。これらはほぼすべてのアプリで絶対に必要になるのでテンプレ化しています。


■Xcodeプロジェクト

プロジェクトのグループはこんな感じで分けます。



ビルド設定については、プロジェクト全体のビルド設定は可能な限り使わないようにして、ターゲットごとのビルド設定 ( Cmd+Option+E ) を主に使います。ターゲットが例え複数に分かれても常に同じ物を使うところはプロジェクト全体の設定を変更します。


■命名規則

大体以下のような方針でやっています。
  • Foundation や UIKit など Apple の使っている命名規則に似せること。似てないシグネチャは即リファクタリング対象。
  • ミススペル、Typo、意味のわからない英単語 ( registとか ) は一文字一単語であろうとも発見した瞬間即リファクタリング対象。英語辞書のソースは http://www.alc.co.jp/ とする。
  • 名前はどれだけ長くなろうとも絶対に省略しないこと。 updUniqUsr とか発見したら即リファクタリング対象。逆に BPSuperDuperViewControllerDidFinishedUberTaskNotification とかは表彰モノ。
    • これを BPSDVCDFUTN とか略した奴が現れたら即 SATSUGAI モノです


■まとめ

ここまで書いておいて何ですが、要するに 一貫した基準があるなら 自分らの好きなようにするのが一番いい のかなと思います。

2010年9月5日日曜日

Three20 の TTURLRequest は POST メソッドのリクエストもキャッシュしてしまう

Three20 には TTURLRequest という Three20 フレームワークの通信関連を一手に引き受けている通信用クラスがあります。基本的には NSURLConnection クラスとほぼ同等なのですが、リクエストを作ったりレスポンスを delegate でハンドルするのがより簡単になるように作られていたり、独自のキャッシュを使用してより効率的なキャッシュをするようになっていたり、様々な点で NSURLConnection より優れていて便利に使えます。

ですがいくつかハマリどころもありまして、今回はそれを紹介します。ちなみに今回使用した Three20 のバージョンは http://github.com/facebook/three20 の default ブランチの 2010/09/05 付での最新コミットです。今後修正される可能性があります。

実は TTURLRequest は、デフォルトでは HTTP メソッドの種類に関係なく、一律すべてのURLをキャッシュするようになってしまっています。そのため、キャッシュ設定をせずに POST メソッドを使って Web API を実行したり、 RESTful な Webアプリ に PUT や DELETE を送ってしまうと、二回目以降のリクエストがキャッシュされてしまいサーバーにリクエストが飛ばなくなってしまいます。回避方法は以下のどちらかを使うと良いです。
  1. 手動でリクエストを作成する際にキャッシュ設定を明示的に指定する
  2. フレームワーク側を書き換えてしまい、 POST, PUT, DELETE 実行時にはキャッシュを無視するようにする

■手動でリクエストを作成する際にキャッシュ設定を明示的に指定する
一番簡単です。以下のようにして明示的にキャッシュを使わないように指定します。
TTURLRequest *request = [TTURLRequest requestWithURL:@"http://mypage.example.com/api/something/post" delegate:self];
request.httpMethod = @"POST";
request.cachePolicy = TTURLRequestCachePolicyNone;

■フレームワーク側を書き換える
でも個人的には GET 以外でリクエスト結果がキャッシュされるのは誰がなんと言おうと不具合だと思っているので、 TTURLRequestQueue.mloadRequestFromCache: メソッドを以下のように書き換えて対応しました。
- (BOOL)loadRequestFromCache:(TTURLRequest*)request {
if (!request.cacheKey) {
request.cacheKey = [[TTURLCache sharedCache] keyForURL:request.urlPath];
}

if (IS_MASK_SET(request.cachePolicy, TTURLRequestCachePolicyEtag)) {
// Etags always make the request. The request headers will then include the etag.
// - If there is new data, server returns 200 with data.
// - Otherwise, returns a 304, with empty request body.
return NO;
//-----------------------------------------------------------------------
// ここから下が変更点
//-----------------------------------------------------------------------
} else if ([request.httpMethod isEqualToString:@"POST"] || [request.httpMethod isEqualToString:@"PUT"] || [request.httpMethod isEqualToString:@"DELETE"]) {
// HTTP POST/PUT/DELETE should not use cache.
// Only HTTP GET can use this cache.
return NO;
//-----------------------------------------------------------------------
// ここまでが変更点
//-----------------------------------------------------------------------
} else if (request.cachePolicy & (TTURLRequestCachePolicyDisk|TTURLRequestCachePolicyMemory)) {
これですべてのHTTPリクエストにおいて、 POST, PUT, DELETE 時にキャッシュが使われなくなります。

2010年8月29日日曜日

自分なりの iPhone アプリ開発手法とかこだわりとか書いてみた

Twitter で vの人こと @voluntas さんに されたので、自分なりのポリシーとかこだわりとか開発手法とかをまとめてみることにしました。今仕事で iPhone アプリの開発を主にやっているので、 iPhone アプリに関する内容が多いですが、それ以外の開発でも使えると思います。

あまり技術的な内容やツールに関する内容はありません。それらは別エントリーにまとめようと思います。


■大前提: 自分を知る
まず何はなくともこっからです。なんだか開発とか全然関係ないじゃないか、怪しい自己啓発じゃねえかと思われるかもしれませんが、敵を知り己をを知れば百戦危うからずと昔のエライ人も言ってます。それにそもそも私がどのような人間なのかを理解しないと、せっかくの開発手法もそのまま真似してはうまく合わない・上手く回らない・賛成できないということになりますので、非常に大事だと思います。ということでさっそくまとめてみました。
  • 恐ろしく几帳面である
    A型人間です。
    Java とか大好きです。逆にカオスなコードが苦手で、 PHP とかは敵です。
    掃除してない @feiz@shin_no_suke の机とか見てると勝手にゴミを捨てたり掃除したくなります。
    Typo とか見つけたら人のコードでも勝手に直します。 Typo するのが嫌なので毎回変数名を決める前に英語辞書を引きます。
  • ゆとりである
    一日の労働時間は八時間までよねー(キリッ
    なのでとにかく楽をしたがりです。
    そもそも体力が小学生並なので徹夜仕事とかできません。そのような事態に陥らないような仕事の進め方が必要になります。
  • やたらこだわる
    好きなモノは徹底的に好きで嫌いなモノは徹底的に嫌いです。なので評価が極端で言動が偉そうです。すみません><
    一歩間違えるとただの頑固者になるので気をつけています。
  • 言動は偉そうだけれども中身は非常に臆病者で弱い
    なのでゆとりを失うと一気に迷走します。余裕を常に持てるようにする必要があります。
    また周りの皆さんの支援ないと生きていけないと思っていますので、「生意気だけれどもイイ奴」ぐらいのポジションに落ち着きたいと思ってます。
  • iPhone は俺の嫁、 Cocoa Touch は神環境
    もう二度と iPhone の無い世界には戻れません。
    10年後ぐらいにはまた別の面白いものがあると思いますが、今のところは iPhone が一番面白いです。
似たような方は私と同じやり方を真似してもいいし、真逆な方は私のやり方と真反対をやればうまくいくのではないかと思います。


■三つの大戦略
私がどういう人間か分かったところで、次は今の仕事のやり方に関して最も根底にある三つの大戦略をまとめてみました。この大戦略にはほとんどツールや言語の話が登場しません。それらは時代が変われば変化してしまうからです。それよりもむしろ、どういう考え方やポリシーで仕事をすれば最も開発生産性が上がるか、というところに着目しています。
  • 徹底的に几帳面にやる
  • 楽をする・楽しむ・楽をさせる・楽しませる
  • 二度とガラケーには戻らないと決心しその通りに行動する
徹底的に几帳面にやる
几帳面なのが武器なので、それを徹底的に生かす方針で開発を進めます。また几帳面さは Objective-C と非常に相性がよいです。たとえばメモリリークが発生してから調査すると非常に大変ですが、発生する前に防ぐことができれば生産性がそれだけ高まるはずです。また KVC/KVO や Core Data など動的言語のような処理を行う箇所では一文字のスペルミスが発見しづらいバグにつながるため、スペルミスチェックを行ったり、命名規則の策定をしたりする必要がありますが、このとき几帳面さが効果的に働きます。
また、余りつまらないところでミスを連発するとテンションも下がるしリズムも崩れ、余裕が無くなってしまい結果として悪いモノができあがってしまうので、それを防ぐためにも几帳面にやるのが効果的だと思ってます。
なにより、これが一番大事だと思うのですが、几帳面にコードを書くのが自分にとって一番楽しく、モチベーションが上がります。つまり逆に言えば、リズミカルにどんどん細かいところを気にしないでガーッとたくさん作るほうが楽しくリズムに乗れてモチベーションも上がる人でしたら、そのようにする方が良いと思います。

楽をする・楽しむ・楽をさせる・楽しませる
四楽運動です(何それ
要するに、
自分が楽したい、仕事量は減らして簡単にしたい
仕事は楽しんでやりたい、面白い技術を使ってやりたい
でも自分ばっかり楽したら申し訳ないし楽した分だけしっかりやって楽をさせてあげよう
それにせっかく作るなら楽しいモノを作った方がいいに決まってるからそうしよ
ということです。
この方針に従うと、私の場合、楽ができて楽しいだけではなく、「このようなすばらしい環境で開発させて貰ってありがとう」という感謝の気持ちと「それなりの成果を出さなくてはならない」という責任感が自然と発生するため、非常に開発効率が高まるようです。

二度とガラケーには戻らないと決心しその通りに行動する
うちの会社でもガラケー向けのWebアプリの開発をやっていて、その際の苦労話を聞く機会があります。また、企画やお仕事の提案などでGREEやモバゲーなどのガラケーアプリを見せて貰うことがあります。そのような経験から思うのは、
ガラケーはIE6とか比較にならないほど苦労するくせにどうしようもないユーザー体験しか提案できない最悪の開発環境
だということです。たとえばメールの文字コードの扱いとか、特定の機種だけ動かないとかで50台以上の実機でテストしないと駄目とか、Flash lite 1.1はまともに使えないからサーバー側でバイナリ操作してSWFを生成して送り返しているとか、そんなネタを聞くたびにこう、ふつふつとしたものが沸いてきます。
その上お金になるからかバッドノウハウだからか知りませんが、驚くほど情報が出回っていない。調べてもなかなか分からない。とにかく開発者に優しくありません。なによりバッドノウハウを運用しても楽しくない上にイライラして嫌なムードになってしまいます。
確かにガラケーの環境はお客さんからお金を取りやすく儲かりやすいため、会社で取り組むには非常に好都合であるとは思うのですが、やはり私は開発者の身である以上、素直に開発者に優しく、さらには素晴らしいユーザー体験を提供できる環境を選びたいのです。さらに思うだけでは駄目で、実際に iPhone の開発で仕事を取って継続的にお金を稼げるようにしなくてはならないので、そうなれるよう実際に行動する必要があります。


■「徹底的に几帳面にやる」を達成するための戦略
  • 面倒くさがらずにリファクタリングする
    リファクタリングは大事です。納得いくまでリファクタリングします。一文字Typoしたら名前変更、名前が後から気に入らなくなったら変更、クラスの構造を変更、メソッドをパブリックに変更、エトセトラエトセトラ。リファクタリングは他の人がコードを読んだ時の理解を助けてくれるだけではなく、自分自身がリファクタリングのためにコードを理解することになり、今後の設計に役立ちます。自分の考えを整理するという意味でも大事だと思います。
  • 細かくスタートして、ちょっと作って確認し、またちょっと作って確認して繰り返す
    特に iPhone での開発は小規模で変更や追加が多く、アップデートも頻繁。必然的にアジャイルな開発が要求されます。そしてなにより、テストケースを書いてテストドリブンで開発するだけの時間的余裕がありません。そんな贅沢なやり方ができるのは長期でずーっとやっていくWebサービスや大規模なシステムだけで、作ってすぐ放棄される事が多い iPhone の開発にはテストドリブンは全く向いていないと思っています。
    しかしながら、品質は当然担保しなければなりません。そこで私は小さく作ってすぐに作った箇所を確認し、また少し作って確認・・・という手法をとっています。正しく作る単位をモジュール化することができれば、一端テストしてその部分の品質が保証されればその後の開発ではテスト済みのモジュール内の事は考えなくて済むからです。テスト自体が大変でコストがかかるので、やはりコーディングの段階で几帳面にバグを出さないようにするというのが一番に思えます。
  • 一番面倒で難しそうなところからスタートする
    ほとんどの技術者の方はこのような手法を取られていると思うのでわざわざ書くほどのものでもないのですが、それでも一番面倒で難しそうなところからスタートする手法は非常に有効です。上記の手法とも合わせ、一番難しそうなところ一番最初に小さくスタートしてすぐにテストし、品質を保証してしまうのが最も有効です。全体の2割のコードに全体の5割ぐらいの時間をかけて作り、残りの半分はコピペしてプロパティをちょっと書き換えるだけでざざーっと作れるようなところにしてしまう、というのが理想だと思ってます。
■「楽をする・楽しむ・楽をさせる・楽しませる」を達成するための戦略
  • 取捨選択をする
    ソフトウェア開発で楽をするための一番よい方法は、作らないことです。取捨選択を適切に行ない、不要な枝を切り捨て、必要な枝・競争力のある枝にすべてのエネルギーを集約することで、個性的なアプリを作り出すことが出来ます。個性的でないアプリは供給過多の市場に埋没します。さらに取捨選択は作業をすすめる最中でも大いに役立ちます。たまにお客さんから「あれもなる早、これもなる早、全部最優先で」とかいうお前日本語間違ってるだろといわんばかりの指示が飛んできたりしますが、これは全く話になりません。「最たるもの」とは常にひとつしか存在しないもので、だからこそまずそれから仕事にとりかかることができ、余計な仕事の順序選択思考を避けることができるのです。私自身が優先順位をつけたり指示を出したりする際にも、最優先がひとつ、次に優先がふたつ、それ以下はすべて後回しか切り落としです。そもそも、三つよりたくさんを記憶して同時に処理できる人はまれです。
  • 偉い人の作ったフレームワークとかライブラリをどんどん使わせていただく
    ソフトウェア開発で楽をするための二番目に良い方法は、すでに完成しているものを取り込むことです。ということで、フレームワークやライブラリなどは積極的に活用します。ライセンスには気をつけないと大変なことになりますので注意ですが、これらのフレームワークやライブラリは私よりも遥かにすごい人達が時間と手間をかけて作り上げられ、テストもしっかり行った上で公開されているものが多いので、私自身が実装するよりも遥かに信頼できると思っています。もちろん人が作ったものなのでバグや合わない仕様はたくさんありますが、ソースコードを読んで修正すればよいのです。ゼロから自分が作るよりは断然良いです。
  • とにかく自動化する
    とかく人間というのは、単純作業をミスすることにかけては超一流です。頻度の差こそあれ誰しも必ず単純作業をミスします。さらに単純作業は集中力という貴重なリソースを消費します。
    二回以上繰り返す単調作業は自動化する。それがたとえ、ソースコードをzipで圧縮してメールでお客さんに送るといった簡単な作業であったとしても、自動化しておくのは非常に役に立ちます。単純作業を避けることで生まれた余剰集中力を、別のところに振り分けることができます。
    自動化する手法がわからなかったら調べて勉強します。ここでちょっとわからなくて面倒だからと避けると後から地獄を見ますし、成長できないので、意地でも調べます。
  • 自分に要求されている責任範囲以上の仕事を引き受けて楽をさせてあげる
    私は最初の会社の新人教育の際に、「自分が要求されている以上の仕事をしろ」と繰り返し教えられました。実際最初の会社はどちらかというとブラックに属する部類だと思いますし、こういう教えは社畜だの何だのと言われているようですが、私はこの「自分が要求されている以上の仕事をしろ」という考えが好きです。
    確かに前の会社で仕事をしている間はこの考えは嫌いでした。なぜなら仕事がつまらない上に余計なことをすると怒られる職場だったからです。バグだらけの共通ライブラリを修正したら余計なことをするなと怒られ、酷い出来栄えのJavaScriptを救済するためにjQueryを突っ込んでみたらこれまた余計なことをするなと言われ。まったくどうしろと・・・。
    しかし今は違います。仕事の内容は全部任せていただいていますし、効率化も推奨されています。画像リソースがお客さんからやってこなければ自分で作り、画面の仕様が決まらなければ私が決めて作ってお渡しし、セールスのためにアプリレビューを行ってくれるブログ一覧をまとめてお渡しするなど、とにかく仕事が円滑に進むと思えばなんでもやります。面倒なときもありますが、それより相手の仕事が遅くってイライラして「あいつが悪い、あいつのせいでプロジェクトが失敗した」とか考える方が嫌です。それにちょっと勝手にやりすぎたかと思っても、意外なほど喜んでもらえます。
    ただし、何でもかんでもこの方法で一人で引き受けていると私の仕事量が破綻するので、この方法で引き受けるのは一過性の仕事だけにしています。定期的にやらなくてはいけない仕事は自動化するか、やり方を教えてあげてやってもらいます。
  • 細部にこだわれるだけのスケジュール上の余裕を常に持つ
    リファクタリングしたり、自分に要求されている範囲以上の仕事をすると、どうしても当初見積りより遥かにたくさんの時間がかかることになります。それにプロジェクトには絶対にトラブルが付きものです。ということで、時間的なゆとりが必要です。私は見積りを提出する際に、自分で余裕を持ってできる見積りに、さらに1.5をかけて提出したりします。提出するときにはこれ時間がかかりすぎじゃないかと言われないかヒヤヒヤしますが、バグを取ったりクオリティを上げたり、突然湧いたトラブルに対処しているうちに、最終的にはそれで大体丁度良くなるので不思議です。お客さんにとっても納期遅延が発生しないため、今のところ大変ご満足頂いております。
    特に iPhone アプリはこの余裕を持ってアプリのクオリティを上げる方針が有効に働きやすい気がします。修正が比較的簡単ですぐに行えるWebアプリに比べ、リリースを急いだとしても、ひとたびバグが原因でリジェクトされるとリリースが一週間遅れ、醜いアプリをリリースするとレビューで酷評されと、ろくなことがありません。
■「二度とガラケーには戻らないと決心しその通りに行動する」を達成するための戦略
  • ガラケーの仕事は意地でも断る
    「ガラケーは嫌いだけれど、仕事だからしょうがないし・・・」冗談じゃありません。幸いにして iPhone 開発で人材募集をしている会社さんも増えましたし、フリーで出来るお仕事の量も増えています。口だけでなくて行動でやらなければなりませんので、まずガラケーの仕事をお断りです。
  • せっかく無理言って iPhone のお仕事をやらせてもらっているので、とにかく全力でやる
    とまぁこんな具合で無理わがままを通しまして何とか iPhone のお仕事を頂きましたので、その分きっちりお仕事して成果を出して次があるようにしなければなりません。こうして後ろを断つとやっぱりやる気が出ます。また、損益にも気が向くようになります。なにせ赤字垂れ流しではあっという間にガラケーに逆戻りです。利益を出さなくては話になりません。
  • 自分に要求されている責任範囲以上の仕事をやる
    一番信用できて一番自分の思い通りに動くのは自分です。まず自分がやることで成功が近づくと考えています。と同時に一番信用できないのも自分なので、人におまかせしたり既存のライブラリなどを活用したりというのも必要になります。なんかすごい矛盾してますが、とにかく自分が一番信用できるけど一番信用できないのです。
  • 次の仕事を持ってきてもらえるように売上とかまで気にする
    「開発は物つくるまでが仕事、売るのは営業の仕事・・・」ガラケーでやってください。 iPhone 開発者の人は少人数かフリーの人が多いため、みなさん作ったものを売るところまで考えてらっしゃるようです。ただし、あくまで開発者の立場として売上を気にすることです。本職の営業や企画の方がいらっしゃるときに、私が営業や企画の真似事を始めると破綻します。
  • 以上のようなわがままを聞いてくれる環境に身を置く
    「んなもんお前に言われなくてもわかってるわ!そんな理想的な環境あるわけねえ!」すみません、ごもっともです><
    結局良い環境で仕事するのが一番で、良い環境は良い人脈と人付き合いがあれば勝手にやってくるみたいなので、まずは勉強会とかに参加しまして良い人付き合いをするのが一番の近道かなぁと思います。
  • 成果はきっちりアピールする
    成果はアピールしないと次につながりませんので、きちんとアピールします。幸いにして私は口から先に生まれてきた人間なので、大声で騒ぐのは得意です!しかしアピールというのはやり方を間違えると単なるスパムや嫌がらせに成り下がってしまいます。TPOをわきまえて正しく成果を大声でアピールしましょう。
    たとえば @iphone_dev_jp に「新作アプリをリリースしました!」なんて大声で流してもイラッとされるばかりですが、リリースして培った経験や技術情報、お役立ち情報を流せば「あいつはできる」と思ってもらえるわけです。
■チームで作業する際の戦略
ここまでは主に自分自身に関する内容の戦略でした。が、実際の仕事はもちろん自分ひとりだけではなく複数の関係者やチームのみなさんと進めることになります。ということで最後にチームで作業する際に私の考える戦略をまとめてみました。
  • 大前提: 人数は少なければ少ないほど良い 能力は高ければ高いほど良い 距離は近ければ近いほど良い
    これが大前提です。とにかくコストを最小限に保ちつつ、時間あたりの開発能力を最大にする必要があります。大規模開発等では当てはまらないかもしれませんが、仕事の総量が比較的少なめで予算が少なく納期が極めて短い iPhone 開発では、開発・デザイナーあわせて殆どの場合3人以内で足ります。その分各個人の責任範囲が非常に広くなるため、すべてのチームメンバーにそれらの責任を全うできるだけのスキルが必要です。誰しも限界があるので最終的には人に聞いたり頼ったりすることになると思うのですが、どこまで個々人が自分自身で調べて解決できるかというところが大事だと思います。何かあったらすぐ頼るような状態では生産性が落ちます。
    「距離は近ければ近いほど良い」というのは物理的にも精神的にもです。物理的に近いほうがやりやすいというのは、極端な例で言えばお客さんが隣のフロアにいるとかです。何か困ったことが発生したときに、メールのやり取りや電話口でのやりとりで解決するより、走って行ってその場で手取り足取り説明したほうが断然早いしうまくいきます。精神的に近いほうがやりやすいというのは、仲がいい方がチームのムードも良くなるし対立が発生しづらいからです。個人的には、良いチームにはデルタフォースとかSASのような特殊部隊のイメージがあります。あのへんの特殊部隊の人が語るチームマネジメント本とか講演会とかあればいいなぁとか思います。
  • 少数・精鋭・見知った仲でチームを結成する
    • 少数になるようにする - 必要最小限の人以外を入れない
    • 精鋭になるようにする - 可能であればそうするのがよいが、できる範囲でやるのであれば、同じ程度の実力の人を集めてチームにする
    • 見知った仲になるようにする - まず知り合う、勉強会とかに行く、一緒に遊ぶ、共通の趣味嗜好を持つ、距離の近いお客さんを選ぶ
    人を増やすのは比較的簡単ですが、減らすのは恐ろしく難しいです。人が増えると責任が薄まります。「俺がいなくてもどうにかなる」と気づいてしまうとヤル気が極端に下がりますし楽しくありません。もちろん、人が足りなくて毎日徹夜しないと追いつかないような状態になっているのはダメで、バランスが重要ですが、基本ほんの少しだけ足りない側に倒すほうが良いかと思ってます。
    チーム全体が精鋭になるようにするというのは、できるのであればそれが最も素晴らしいことだと思いますが、まぁ現実的に考えて難しいと思います。そんな手段があれば私がまず知りたいです>< で、現実的な作戦としては、チーム全体の実力をできる限り均一化すると言うことが挙げられます。チームメンバーの実力にほとんど差がない状態が、一番チームメンバー個々の能力を最大限に発揮させ、責任感を最大限に発揮させることができると思うからです。一人だけ精鋭がいてもその人に負荷と責任が集中し、他の人はアイツがいるからと思ってしまいます。逆に一人だけおちこぼれるとみんながその一人のせいにして責任を放棄します。
    見知った仲になるようにするために、例えば社内で月一ピザでも食べながら勉強会をやるとか、 誰か の家に飲みに行くとか、お昼ごはんを一緒に食べるとか。ここが一番難しい気がしますが、一番大事なところだと思います。仲がよければちょっとやそっとのミスや負荷があっても許してあげられるようになります。逆に険悪になるとほんの少しのミスや負荷が許せなくなり、さらに事態が悪化します。
    ただし、仲が良いのと馴れ合うのは絶対に違うと思います。馴れ合い始めると新しい人が入ってこれなくなり、仕事に張りがなくなります。これもバランスですね。
  • 開発チームとステークスホルダーが直接コミュニケーションできるようにする
    いくつかプロジェクトをやって気づいたことがこれで、開発チームとステークスホルダー(最終決定権保持者、各種画像リソースなどのリソース提供者)の距離が遠いとプロジェクトが失敗しやすくなります。距離が遠いというのは、間に仲介役 - たとえば営業の人だとか - が入るということです。人ほど信用ならない情報伝送経路はありません。必ず情報の劣化・嘘の混入・膨大な遅延が発生します。このような伝送経路を使うのをやめて、直接ステークスホルダーの人とSkypeでやり取りできるようになると、効率が格段によくなります。ミスも発生しません。
  • それでもどうしても何ともならないとき
    • 人数が増えてしまった - チーム/責任範囲を分割して可能な限り少数にする、または別の仕事を取ってきてそちらにチームを分割する
    • スキルが低い人と一緒に仕事をする必要がある - 低い人の理解に全員が合わせる。
    • 顔も名前も知らない人がチームに入ってきた、どうしてもウマが合わない人がいる - そこでどう仲良くなるかというのが本当にカッコイイ大人のやることではないか(キリッ
    • 物理的に距離が遠い - ツールを最大限に活用する。ツールを導入できるように先方をうまく説得する。
    • ステークスホルダーと直接話しにくい - 無理矢理にでも直接話すのを試みる
    すみません、かなり無茶苦茶です>< というのも私自身ここに書かれているような事態にうまく対処できないからです。実際これらの問題をうまく乗り切られる人は、人間力があるといいますか、磨かれた大人であるといいますか、優れたマネージャーさんであると思います。現実問題、こういった問題は数限りなく存在しますので、具体的な対処策は私自身学び取りたいところです。ですが、絶対に勘違いしてはならないのが、これらはあくまで「対処療法」であって、「根本的な治療」ではないということです。最初から対処療法が必要でないような環境を作るために尽力し、そのような環境で仕事をしていれば、これらの問題はそもそも発生せず、そのため余力を生むことができます。発生した余力はさらに理想的な環境を整備するために使ったり、自分自身のスキル向上のために使えます。
    これを体の健康にたとえるならば、毎日暴飲暴食せず野菜を取り適度な運動を行ない十分な睡眠をとることで病気にならない様にするのが一番であって、自堕落な生活を送った結果重い病気にかかったので医者に行って治してもらうというのを繰り返すようでは体がどんどんぼろぼろになっていきます。体の健康に例えると、前者を行う人が尊敬され後者は笑いものにされるのですが、チーム運営となると前者は魔法か理想論と馬鹿にされ後者の出来る人が大人だと尊敬されているようで、どうも私はしっくりきません。何事も予防する、予防できる環境を作るのが一番です。発生してから耐えるのは美徳ではなくやむを得ないことです。
  • 課題: どうしても合わない人をどう追放するか
    これは世界全国、人とチームがある限りの永遠の課題だと思います。本当はこんなことをしないでよいのであれば理想なのですが、それでもどうしても人が余ったりチームに合わないからという理由で人を外さなければならないことがあります。アメリカ人みたいに非情にズバズバ首を斬ると、後から衝突が発生したりチームの財産を平気で奪って逃げたりされます。かといって日本人みたいに何時までも何時までも衝突を恐れて首を切れないと、チーム全体の死活に関わります。まったく、どうするのがいいんでしょう・・・><
■まとめ

3行でまとめると、楽しく生産性の高い仕事をするには、
責任感を持つ/持たせるのが大事
ゆとり大事
バランス大事
こんなところですかね。

2010年8月15日日曜日

CALayer を使って UIImage を描画する

UIImage を高速で描画する必要がある案件に遭遇したため、 CALayer を使ってみました。 CALayer と聞くとなにやら難しい感じがしますが、実際に使ってみると非常に簡単で高速です。

CALayer を使うと良い場面は以下のような場合です。
  • 画像を大量に描画する必要がある
  • 画像を高速に描画する必要がある
  • 画像を高速に変形・移動する必要がある
  • CGContextDrawImage を今使っている箇所がある
とくに変形に対して非常に強いです。 CGContextDrawImage で変形後の UIImage を再度描画し直したりするのに比べると、 CALayer の変形は格段に高速に動作します。


■実際に描画してみる

まず最初に <QuartsCore/QuartsCore.h> をインポートします。 QuartzCore.framework をプロジェクトに追加するのも忘れないようにしましょう。

準備ができたので描画します。 CALayer.contents プロパティに CGImageRef を渡すと後は全部勝手にやってくれます。簡単でしょ?
- (void)viewDidLoad {
    // UIImage* 型のプロパティ self.image があると仮定して・・・
    CALayer *l = [CALayer layer];
    l.contents = self.image.CGImage;
    l.position = CGPointMake(255, 255) // l.position はデフォルトではレイヤー中央の座標になります
    [self.view.layer addSubLayer:l];
}
CGContextDrawRect のような Core Graphics (Quartz) の関数を使用すると、座標系が左下基準になるため、 UIImage を描画する際にコンテキストの上下を反転してやらないと画像が上下反対に表示されてしまう問題がありますが、 CALayer はこの座標系の差異も勝手に考慮に入れてくれるので、 UIImage の CGImage プロパティをそのまま渡すだけでよく、ラクチンです。


■アニメーションしてみる

もともと CALayer は Core Animation フレームワークのクラスですから、当然アニメーションにも対応しています。といいますか、何もしないでそのまま CALayer のプロパティを変えると勝手にアニメーションします。 http://developer.apple.com/iphone/library/documentation/Cocoa/Conceptual/CoreAnimation_guide/Articles/AnimatingLayers.html の Animation > Implicit Animation あたりに記載されている内容がそれです。
// 既にこの l が addSubLayer されている場合、
// これだけで勝手にアニメーションします
l.transform = CGAffineTransformCreateScale(1.1, 1.1);
が、ときどきこのアニメーションが邪魔になる場合があります。そんなときは
// CATransaction というクラスを使用して, 一時的にレイヤーのアニメーションを切ります
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
                 forKey:kCATransactionDisableActions];
[aLayer removeFromSuperlayer];
[CATransaction commit];
// CATransaction というクラスを使用して, 一時的にレイヤーのアニメーションの時間を変化させます
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:10.0f]
                 forKey:kCATransactionAnimationDuration];
theLayer.zPosition=200.0;
theLayer.opacity=0.0;
[CATransaction commit];
こんな具合でアニメーションを調整できます。


■まとめ

たったのこれだけで画面上に高速でアニメーションもできる画像を描画することができます。お絵かきソフトなどで、画面上にユーザーが任意の画像を挿入できるようにしたい、と言ったときに大変役立ちますのでおすすめです。是非試してみてください。

2010年7月7日水曜日

Core Graphics (Quartz) のみで日本語文字列を描画するライブラリのヘッダファイルを書いてみた

とある理由で UIKit の描画機能が使えず、 Core Graphics のみで文字列の描画処理を行わなければならないことになってしまったので、適当に調べてみました。


■ことのはじめ

Core Graphics の機能だけで日本語の文字列を描画する方法については、既に先人の方々が調べて記事にまとめてくださっていたので、そちらを見ていただければ大丈夫です。
http://iphone-dev.g.hatena.ne.jp/ktakayama/20100129
http://d.hatena.ne.jp/r_kurain/20100316
基本的にはこちらで紹介されている方法に従って進めていけば困ることはありません。ただし、描画した文字が上下反対になることがありますので、 CGContextSetTextMatrix を使う箇所を調整したりする必要があるかも。

で、最大の問題になるのがこれらの記事で紹介されている CGFontGetGlyphsForUnichars と呼ばれる関数です。この関数を使えば楽に日本語文字列を文字化けすることなく描画することができるのですが、あろうことかこの関数はプライベートAPIであり、使うとリジェクトされてしまうらしいです。ということで別の作戦をとらなければなりません。

上記の記事にいろいろな対処方法が載っているのですが、私は一番単純に
http://www.mexircus.com/codes/GlyphDrawing.mm
というライブラリを自分のアプリに組み込む方法を試してみることにしました。この方法を使えば、 CGFontGetGlyphsForUnichars でまず作って動くようにしてから、 CMFontGetGlyphsForUnichars と書き換えるだけでそのまま動作するので楽です。

問題はこのライブラリ、mmファイルしか用意されていないのでそのままではうまく使えません。適当にヘッダファイルを用意してやる必要があります。


■と言うわけで書いたもの

と言うわけで GlyphDrawing.h を書いてみました。
http://gist.github.com/466297

ライセンスは元の GlyphDrawing.mm に準じますが、 GlyphDrawing.mm のライセンスが不明なので、不安ならばまず作者さん (http://mexircus.com/blog/) に一言聞いてみるのがいいと思います。


■使い方
  1. http://gist.github.com/466297http://www.mexircus.com/codes/GlyphDrawing.mm をダウンロードします。
  2. GlyphDrawing.mm のインポートを以下のように書き換えます:
    //#import <Foundation/Foundation.h>
    #import "GlyphDrawing.h"
  3. GlyphDrawing.hGlyphDrawing.mm をプロジェクトに追加してビルドします。
  4. CGFontGetGlyphsForUnichars の代わりに CMFontGetGlyphsForUnichars を呼びだせばOKです。


■実際に使ったコードの例
NSString *message = @"本日は快晴なり";

// フォントを設定
// ついでにmessageのサイズも取得
// ただしsizeWithFontは UIGraphics の機能なので、本当に Core Graphics だけで描画したいならここで使ってはいけません
UIFont *font = [UIFont fontWithName:@"HiraKakuProN-W6" size:32.0];
CGRect messageRect = [message sizeWithFont:font];
CGFontRef fontRef = CGFontCreateWithFontName((CFStringRef)font.fontName);
CGContextSetFont(c, fontRef);
CGContextSetFontSize(c, font.pointSize);

// Glyphを作成
size_t length = [message length];
CGGlyph glyphs[length];
UniChar chars[length];
[message getCharacters:chars range:NSMakeRange(0, length)];
CMFontGetGlyphsForUnichars(fontRef, chars, glyphs, length);

// 文字列が上下反対になるのを防止する
CGAffineTransform transform = CGAffineTransformMakeScale(1.0, -1.0);
CGContextSetTextMatrix(c, transform);
CGContextTranslateCTM(c, 0, messageRect.size.height/2);

// 描画
CGFloat x = 100.0f;
CGFloat y = 100.0f;
CGContextShowGlyphsAtPoint(c, x, y, glyphs, length);

2010年6月26日土曜日

別スレッドで NSURLConnection を使うときのメモ

http://twitter.com/griffin_stewie/status/17022582070

@griffin_stewieさんにご指摘いただいたので調べてみました。

別スレッドでNSURLConnectionを使おうとすると、そのままではNSRunLoopのモードの問題なのか、上手い具合にデータを受信することが出来ません。私はメインスレッドでNSURLConnectionを動かすようにして難を逃れたのですが、それ以外にも教えていただいた方法を用いて、以下のように別スレッドのNSRunLoopのモードを変更して対応すると良いみたいです。
    NSURLRequest *theRequest = [NSURLRequest requestWithURL:iTunesURL];
// create the connection with the request and start loading the data
rssConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
// This creates a context for "push" parsing in which chunks of data that are not "well balanced" can be passed
// to the context for streaming parsing. The handler structure defined above will be used for all the parsing.
// The second argument, self, will be passed as user data to each of the SAX handlers. The last three arguments
// are left blank to avoid creating a tree in memory.
context = xmlCreatePushParserCtxt(&simpleSAXHandlerStruct, self, NULL, 0, NULL);
if (rssConnection != nil) {
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!done);
}
runMode:beforeDateを使うわけですね。なるほど。

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で計測すればさらに顕著な違いになると思うのでおすすめです。

第 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年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でのプログラミングを既にやめちゃっていると聞いたのでメンテがどうなるのか不安です。

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と同様に綺麗に表示され、さらにタップするときちんとアクションが実行されるようになりました!

2009年11月1日日曜日

PDFをiPhoneから生成するサンプルプロジェクトを公開いたしました

本編はこちら
エラーハンドリング編はこちら

ここまでの成果をgithubにアップいたしました。ダウンロードしてXcodeを開き、ターゲットのビルド設定から開発用コードサインをご自身のものと入れ替えてビルドすれば、すぐにPDF作成がお試しいただけます。
http://github.com/akisute/iPhonePDF

ライセンスはlibHaruと合わせてZLIB/LIBPNG Licenseにしてみました。


当方でも動作確認は行っておりますが、何か不具合ございましたらご一報いただけると幸いです。

2009年10月31日土曜日

libHaru on iPhone: Objective-Cでエラーハンドリング



前回記載できなかったエラーハンドリングについて軽くさわってみました。


■libHaruのエラーハンドリング
http://libharu.org/wiki/Documentation/Error_handlingに詳しく書いてあります。要するに、一番最初のPDFドキュメントを生成するところでエラーハンドラ関数を渡したらあとはエラーが発生するたびにその関数が呼び出されるみたいです。
// まずはヘッダファイルの中で
// ユーザーデータ用の構造体の宣言と、エラーハンドラ関数のプロトタイプ宣言をしておく

typedef struct _PDFService_userData {
HPDF_Doc pdf;
PDFService *service;
NSString *filePath;
} PDFService_userData;

// エラーハンドラ関数のシグネチャは常にこうでなければならない
// 関数名は自由に決めて良いが、引数と返り値は決められている
void PDFService_errorHandler(HPDF_STATUS error_no,
HPDF_STATUS detail_no,
void *user_data);
// 注意点として、エラーハンドラ関数はHPDF_Newより前の行で実装しておかなければならない
// (プロトタイプ宣言しておいてもだめみたいです)
void PDFService_errorHandler(HPDF_STATUS error_no,
HPDF_STATUS detail_no,
void *user_data) {
// ユーザーデータを取得する
// C言語の構造体からでもObjective-Cのクラスが取り出せますよ
PDFService_userData *userData = (PDFService_userData *)user_data;
HPDF_Doc pdf = userData->pdf;
PDFService *service = userData->service;
NSString *filePath = userData->filePath;

// エラーハンドリング後、PDF関連の処理を続行したい場合は
// このHPDF_ResetError関数を呼び出す必要がある。
// これを呼び出さないと後続の処理がうまくいかない。
HPDF_ResetError(pdf);

// 逆にエラーハンドリング後、PDF関連の処理をすべて中断したい場合には
// HPDF_Free関数を使ってpdfオブジェクトを解放するとよい。こうすると、以後の
// HPDF関連の関数はすべて何もしないで終了してくれる。ただし、同じpdfオブジェクトを
// 2回HPDF_Freeしてしまうとエラーになるため、HPDF_HasDocを使って制御すること
HPDF_Free(pdf);
}

// あとはPDFドキュメント生成時に関数とユーザーデータを引数として渡す
int main(int argc, char** args) {
PDFService_userData userData;
HPDF_Doc pdf = HPDF_New(PDFService_defaultErrorHandler, &userData);
userData.pdf = pdf;
userData.filePath = @"/var/tmp/hogehoge.pdf";
// 以下省略
}


■CとObjective-Cをつなげる
C言語的にはこれでよいのですが、Objective-C、ひいてはCocoa Touchフレームワーク的にはこういうエラー処理はdelegateにしてしまったほうが便利です。たとえば、PDFファイルを生成するためのObjective-CクラスPDFServiceに、PDFServiceDelegateを持たせて運用してみましょう。libHaruのエラーハンドラ関数を以下のように作ってみます。
void PDFService_defaultErrorHandler(HPDF_STATUS   error_no,
HPDF_STATUS detail_no,
void *user_data)
{
// ユーザーデータを展開
PDFService_userData *userData = (PDFService_userData *)user_data;
HPDF_Doc pdf = userData->pdf;
PDFService *service = userData->service;
NSString *filePath = userData->filePath;

// 以後のPDF作成処理をすべてストップする
HPDF_Free(pdf);

// Delegate呼び出し
if (service.delegate) {
[service.delegate service:service
didFailedCreatingPDFFile:filePath
errorNo:error_no
detailNo:detail_no];
}
}
あとはこのDelegateを適当なViewControllerなどに準拠させて、
#pragma mark -
#pragma mark delegate method
- (void)service:(PDFService *)service
didFailedCreatingPDFFile:(NSString *)filePath
errorNo:(HPDF_STATUS)errorNo
detailNo:(HPDF_STATUS)detailNo
{
NSString *message = [NSString stringWithFormat:@"Couldn't create a PDF file at %@\n errorNo:0x%04x detalNo:0x%04x",
filePath,
errorNo,
detailNo];
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:@"PDF creation error"
message:message
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] autorelease];
[alert show];
}
とすると、冒頭のイメージのようになります。

2009年10月24日土曜日

libHaruを使ってiPhoneアプリからPDFを作成する

ある日突然、iPhoneからPDFファイルを作ったら面白そうだと思ったので、早速試してみることにしました。既に先駆者の方がいらっしゃるかと思ったのですが、調べてもそれらしい資料が無いので自力でなんとかしてみることにしました。需要はあまり無いかと思いますが、何かのお役に立てば幸いです。


■PDFファイルを生成する方法
まずはPDFファイルを生成する方法を知らなければ話になりません。ということで、調査しました。
  • 一からライブラリを自作する
    jspdf(http://code.google.com/p/jspdf/)などという漢気あふれるライブラリがあるのですから、やってやれないことはない!と思い、さっそくPDFの仕様書(http://www.adobe.com/devnet/pdf/)をAdobe社からダウンロードしてきたのですが、31MB, 500ページ以上あります。なにこれこわい。
  • Adobe PDF Library(http://www.adobe.com/devnet/pdf/library/ http://www.est.co.jp/pdfl/index.html
    本家のライブラリです。日本ではイースト社がライセンスしてるみたいですが、どう見ても有償です。チェンジお願いします。
  • libHaru(http://libharu.org/wiki/Main_Page
    こうして散々探した挙句ついに見つけた至高のPDFライブラリがこちら。C言語で書かれており、iPhoneでもバッチリ動きそうです!もちろんオープンソース!素晴らしい!
ということで、今回はlibHaruを使ってみようと思います。


■libHaruをXcodeでビルドする
1.libHaruのソースコードをダウンロードしてXcodeプロジェクトに追加
最新のソースコードをhttp://libharu.org/wiki/Downloadsからダウンロードしてきて解凍します。このまま自分のMacで使えるようにするだけなら. configureしてmakeしてmake installすれば一発なのですが、あいにくそれではiPhoneで動かすことが出来ません。
includeディレクトリにヘッダファイルが、srcディレクトリにCソースコードが入ってますので、これら全部を自分のXcodeプロジェクトにコピーして取りこみます。


2.libpngのソースコードをダウンロードしてXcodeプロジェクトに追加
libHaruを用いてPDFに画像を出力したいときには、別途libpngが必要になります。そこで、libpngも一緒にビルドすることにします。libpngのソースコードはhttp://www.libpng.org/pub/png/libpng.htmlから入手できますので、これをダウンロードしてきてXcodeに追加してビルドすればよいのですが、ここではもっと簡単で確実に動く方法をご紹介します。そう、「既にlibpngをビルドして使っている他のiPhoneプロジェクトからパクる」です!(もちろん、きちんとライセンスに違反していないことを確認しましょう)
おあつらえ向きなことに、cocos2d for iPhoneの0.8.1以降(http://code.google.com/p/cocos2d-iphone/source/browse/#svn/trunk)では内部的にlibpngを使用しています。これを真似しない手はありません。さっそくcocos2d for iPhoneの最新ソースを取得し、Xcodeでプロジェクトを開いて、Support/external/libpng以下のソースをコピーして自分のプロジェクトに追加しましょう。


3.libHaruをconfigureする
. configureで動けばいいのですが、多分. configureしても今の自分のMacの環境に合わせてConfigureされてしまい、iPhoneでは動かないだろうと思ったので、自分の手でconfigureすることにしました。といっても、直すのは一箇所だけです。includeディレクトリに入っていたhpdf_config.h.inファイルを以下のように修正します。
/* include/hpdf_config.h.in.  Generated from configure.in by autoheader.  */

/* Define to 1 if you have the header file. */
#undef HAVE_DLFCN_H
#define HAVE_DLFCN_H 1

/* Define to 1 if you have the header file. */
#undef HAVE_INTTYPES_H
#define HAVE_INTTYPES_H 1

/* Define to 1 if you have the `png' library (-lpng). */
#undef HAVE_LIBPNG
#define HAVE_LIBPNG 1

/* Define to 1 if you have the `z' library (-lz). */
#undef HAVE_LIBZ
#define HAVE_LIBZ 1

/* Define to 1 if you have the header file. */
#undef HAVE_MEMORY_H
#define HAVE_MEMORY_H 1

/* Define to 1 if you have the header file. */
#undef HAVE_STDINT_H
#define HAVE_STDINT_H 1

/* Define to 1 if you have the header file. */
#undef HAVE_STDLIB_H
#define HAVE_STDLIB_H 1

/* Define to 1 if you have the header file. */
#undef HAVE_STRINGS_H
#define HAVE_STRINGS_H 1

/* Define to 1 if you have the header file. */
#undef HAVE_STRING_H
#define HAVE_STRING_H 1

/* Define to 1 if you have the header file. */
#undef HAVE_SYS_STAT_H
#define HAVE_SYS_STAT_H 1

/* Define to 1 if you have the header file. */
#undef HAVE_SYS_TYPES_H
#define HAVE_SYS_TYPES_H 1

/* Define to 1 if you have the header file. */
#undef HAVE_UNISTD_H
#define HAVE_UNISTD_H 1

/* debug build */
#undef HPDF_DEBUG
#ifdef DEBUG
#define HPDF_DEBUG 1
#endif

/* debug trace enabled */
#undef HPDF_DEBUG_TRACE
#ifdef DEBUG
#define HPDF_DEBUG_TRACE 1
#endif

/* libpng is not available */
#undef HPDF_NOPNGLIB

/* zlib is not available */
#undef HPDF_NOZLIB

/* Define to the address where bug reports for this package should be sent. */
#undef PACKAGE_BUGREPORT

/* Define to the full name of this package. */
#undef PACKAGE_NAME

/* Define to the full name and version of this package. */
#undef PACKAGE_STRING

/* Define to the one symbol short name of this package. */
#undef PACKAGE_TARNAME

/* Define to the version of this package. */
#undef PACKAGE_VERSION

/* Define to 1 if you have the ANSI C header files. */
#undef STDC_HEADERS
#define STDC_HEADERS 1

/* Define to `unsigned int' if does not define. */
/* #undef size_t */
書き換えたら、ファイル名をhpdf_config.hに変更します。


4.Xcodeから新規ビルドターゲットを作成する
ソースコードの準備が出来たら、次はソースコードをビルドしてlibharu.aとlibpng.aを生成するためのビルドターゲットを新規に作成します。ビルドターゲットは使い方を覚えると非常に便利です。




最初にlibpngのビルドターゲットを作ります。libpngのビルドターゲットは、2.でご紹介したとおりcocos2d for iPhoneの最新ソースのXcodeプロジェクト内に既に用意されていますので、これを真似して作るとよいです。ただし、libpngをビルドする際にzlib.dylibが必要になりますので、自分のプロジェクトにzlib.dylibを追加するようにしておいてください。最初からiPhone SDKの一部として用意されています。



続いてlibHaruのビルドターゲットを作成します。こんな感じです。



最後に、libharu.aを自分のiPhoneプロジェクトの依存ライブラリに追加します。


5.レッツコンバイン!
これでビルドする準備が整いましたので、いよいよビルドします。
緊張の一瞬!Cmd+Bをプログラムドライブ!!



できました!!!!


■Hello, libHaru!
無事にビルドが完了いたしましたので、次はいよいよPDFを生成してファイルに出力してみます。
// TEST CODE: testing libharu
NSString *path = nil;
const char *pathCString = NULL;
NSLog(@"[libharu] PDF Creation START");
HPDF_Doc pdf = HPDF_New(NULL, NULL);
NSLog(@"[libharu] Adding page 1");
HPDF_Page page1 = HPDF_AddPage(pdf);
NSLog(@"[libharu] SetSize page 1");
HPDF_Page_SetSize(page1, HPDF_PAGE_SIZE_A4, HPDF_PAGE_LANDSCAPE);
NSLog(@"[libharu] TextOut page 1");
HPDF_Page_BeginText(page1);
HPDF_UseJPFonts (pdf);
HPDF_UseJPEncodings (pdf);
HPDF_Font fontEn = HPDF_GetFont(pdf, "Helvetica", "StandardEncoding");
HPDF_Font fontJp = HPDF_GetFont(pdf, "MS-Mincyo", "90ms-RKSJ-H");
HPDF_Page_SetFontAndSize(page1, fontEn, 16.0);
HPDF_Page_TextOut(page1, 100.00, 100.00, "Hello libHaru!");
HPDF_Page_SetFontAndSize(page1, fontJp, 16.0);
HPDF_Page_TextOut(page1, 100.00, 60.00, [@"はろー日本語" cStringUsingEncoding:NSShiftJISStringEncoding]);
HPDF_Page_EndText(page1);
NSLog(@"[libharu] Path drawing page 1");
HPDF_Page_SetLineWidth(page1, 4.0);
HPDF_Page_SetRGBStroke(page1, 1.0, 0, 0);
HPDF_Page_Rectangle(page1, 200, 200, 40, 150);
HPDF_Page_Stroke(page1);
NSLog(@"[libharu] PNG image drawing page 1");
path = [[NSBundle mainBundle] pathForResource:@"portrait2"
ofType:@"png"];
pathCString = [path cStringUsingEncoding:NSASCIIStringEncoding];
NSLog(@"[libharu] LoadPngImageFromFile path:%@\n pathCString:%s", path, pathCString);
HPDF_Image image = HPDF_LoadPngImageFromFile(pdf, pathCString);
HPDF_Page_DrawImage(page1, image, 300, 50, 245, 319);

// Get documents directory
NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
path = [arrayPaths objectAtIndex:0];
path = [path stringByAppendingPathComponent:@"test.pdf"];
pathCString = [path cStringUsingEncoding:1];
NSLog(@"[libharu] SaveToFile path:%@\n pathCString:%s", path, pathCString);
HPDF_SaveToFile(pdf, pathCString);
NSLog(@"[libharu] Freeing PDF object ");
HPDF_Free(pdf);
NSLog(@"[libharu] PDF Creation END");

これを実行すると、アプリのDocumentディレクトリにtest.pdfが生成されるはずです。


■実機で試してみる
シミュレータでは動くようになりましたが、実機で動かなければ何の意味もありません。ということで、いよいよ実機でテストしてみます!生成したPDFファイルを表示するためのUIWebViewを作成し、実機で表示してみた結果がこちら。



Hooray!!


■さて次は
いよいよ次からが本番、UIViewの中身をPDFに変換して出力する機能を作ってみようと思います!

2009年10月23日金曜日

iPhone向けに最適化されたPNGをlibpngで扱う方法

メモ。
iPhoneアプリのバンドルに同梱したPNGファイルは、ビルド時に最適化処理が行われてしまうため、そのままではlibpngで読み込むことが出来ません。iPhone向けに最適化されたPNGをlibpngで扱う方法は、いまのところ二つ。


1:最初から最適化をしないようにする
参考にしたページはこちら。
http://d.hatena.ne.jp/wasabi-arts/20090301/1235856525


このように、IPHONE_OPTIMIZE_OPTIONS=-skip-PNGsを追加するか、


またはこのように、最初っから圧縮をしないように設定する。


2:最適化したファイルをいったんUIImageで読み出して、再度PNGでファイル書きだしする
いったんUIImageを使って当該ファイルをロードして、ファイルにpng形式で書き出せばよいらしいです。
(@nakamura001さんありがとうございます!)

[2009/10/24 22:20追記]
@nakamura001さんがご自身のブログで検証結果をアップされてます。
http://d.hatena.ne.jp/nakamura001/20091024/1256371800

UIButtonからPNG画像を抜く方法ですが、これを応用すればUIButtonに最適化されたPNGを使用していても普通のPNG画像を取得することが出来ます。私の環境でもテストしたところ、うまくいきました!