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

2011年2月28日月曜日

UIWebView のスクロールを制御するためのプロパティを書いてみた

UIWebView にどうして scrollEnabled プロパティがついてねえんだ Apple のチンパンジー野郎!とお嘆きの全国1000万の iOS 開発者の皆様、こんばんわ。もちろん私もその一人であります。

嘆いていてもしょうがないので何とかスクロールを制御する方法を・・・と思って探していたら、すでに2009年の地点で @nakamura001 さんがこんなブログを書いてらっしゃいました。

http://d.hatena.ne.jp/nakamura001/20090520/1242837408

が、遷移先で詳解されている

http://praveenmatanam.wordpress.com/2009/04/03/how-to-disable-uiwebview-from-user-scrolling/

のコードが正直いまいちなのです。何がいまいちって、せっかくのCocoa環境であるにも関わらず、わざわざobjc/runtime.hなんていう低レベルなC言語の関数を使っています。別にパフォーマンスがタイトな場所でもないですし、かっこよくCocoaっぽく書き直してみました。ということで書き直したコードがこちら↓

https://gist.github.com/846258

で、このAdditionを導入するとですね、
UIWebView *webView = [[[UIWebView alloc] initWithFrame:frame] autorelease];
webView.webViewScrollEnabled = NO;
みたいな書き方ができてハッピーになれます。

内部的には NSInvocation を使っています。 Mac OS X 10.5 (iOS 2.0) から存在するこのクラス、本当に便利で、ぶっちゃけ呼び出し対象のシグネチャさえわかれば何でも呼び出せるスグレモノです。フレームワークを作るときなど、呼び出し先のシグネチャしかわからない状況下で対象のメソッドを呼び出す時などに便利な感じかもです。その上 NSInvocationOperation を使ってそのまま並列化もできたりして。
NSMethodSignature *sig = [subview methodSignatureForSelector:selector];
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:subview];
[invocation setSelector:selector];
[invocation invoke];
BOOL result = YES;
[invocation getReturnValue:&result];
return result;

2011年2月9日水曜日

UITableView の cell の中に UITextView を配置したとき発生する問題とその対処方法

たとえば Twitter クライアントや SMS クライアントのように、短い文章を入力させるような画面を作りたいとき, Grouped Style な UITableView の cell の中に UITextView を配置して画面を作る事が良くあるかと思います。通常、この作りでほとんど問題はありませんが、特定の条件下で問題が発生することがわかりましたので、回避策をメモしておきます。


■問題

この問題が発生するのは、以下の条件を満たしたときです。
  1. UITableView の cell の中に UITextView を配置する。
  2. UITextView に、縦スクロールが発生するぐらい長い文字列を入力する。
  3. UITextView の中ほどにカーソルを移動する。
  4. UITableView と UITextView の scrollEnabled プロパティを NO に設定する。
  5. UITextView の text プロパティを変更する。
すると, UITextView および UITableView の scrollEnabled プロパティを NO に設定しているにもかかわらず、勝手に UITableView がスクロールを起こしてしまいます。 UITextView の text プロパティを書き換えたら勝手に UITextView が一番下までスクロールしてしまうのはデフォルトの挙動であり、これは問題ありません. UITextView を単品で使っているときは, scrollEnabled プロパティを NO に設定することでスクロールを発生させずに text を書き換えることが可能なのですが, UITextView を UITableView の中に含めたときのみ, scrollEnabled プロパティが無視されてしまい、この問題が発生してしまうようです。

カスタムキーボードを用意したり、本文中のURLを短縮して差し替えたりなど、結構 UITextView の text プロパティを直接書き換えたい要件があったりするので、このままでは困ります。


■解決方法

そこで、相当な荒技ですが、以下のようにして解決を図ることが可能です。
  1. UITableView の cell の中に UITextView を配置する。
  2. UITextView をいったん removeFromSuperview する。
  3. UITextView をいったん UITableView 以外の場所に addSubview する。
    • UIWindow の直下などがおすすめ。
  4. UITextView の scrollEnabled プロパティを NO に設定する。
  5. UITextView の text プロパティを変更する。
  6. UITextView の scrollEnabled プロパティを YES に設定する。
  7. UITextView を元の view に addSubview する。
  8. UITextView を first responder にする。
コードにすると以下のようになります:
UIView *parentView = textView.superview;
[self.view.window addSubview:textView];
textView.scrollEnabled = NO;

textView.text = [currentText stringByReplacingCharactersInRange:selectedRange withString:shrunkenURLString];
textView.selectedRange = NSMakeRange(selectedRange.location + [shrunkenURLString length], 0);

textView.scrollEnabled = YES;
[parentView addSubview:textView];
[textView becomeFirstResponder];
こうすることで, UITableView が勝手にスクロールしてしまう問題を回避することが可能です。

2011年2月2日水曜日

"Failed to launch simulated application: Unknown error." が発生したときの対処法



iPhone シミュレーター起動時に表題のエラーが発生したときの対処法です。


表題のエラーは文字通り原因不明の問題が発生したときに表示されるのですが、問題のうちの一つに「バンドルリソース内にMac OSが使用する予約されたリソース名が含まれていると発生する」というものがあります。

たとえば
Contents
Resources
などの名前のディレクトリやファイルをバンドルリソース内に作成してしまうと、ビルドは通るのですがシミュレーター実行時にエラーが発生するようです。これらの予約された名前は使わないようにすると良いです。

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 操作をしても安全
という、まさにかゆいところに手が届く良さがすべてあります。惚れる。もうおそらく当分の間はこれ以上の通信ライブラリが現れることはないんじゃないかと言い切って良いぐらいすてきです。