2011年11月19日土曜日

UIWebView.scrollView に対して KVO を使うと色々面白い

iOS 5より、UIWebViewにscrollViewプロパティが追加され、たとえばスクロールを無効にしたりステータスバーをタップしても一番上に戻らないようにしたりなど、UIWebViewのスクロール周りの処理を外から自由に触れるようになりました。ですが便利なのはこれだけではありません。KVOの仕組みを使うことで、さらにUIWebViewを便利に使うことができます。ここでは私が使っている中で一番のおすすめをご紹介します。

■UIWebViewの描画しているHTMLのcontentSizeを非同期的に、リアルタイムで取得する
UIWebview.scrollViewのcontentSizeプロパティは、UIWebViewの描画しているHTMLの大きさ(contentSize)と同じ値になります。この性質を利用して、contentSizeプロパティにKVOを貼ると、UIWebViewの描画しているHTMLの大きさ(contentSize)が変わったタイミングで通知を受け取ることができます。こんなコードになります。
- (void)viewWillAppear:(BOOL)animated
{
  [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];
  [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://akisute.com"]]];
}
- (void)viewWillDisappear:(BOOL)animated
{
  [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  NSLog(@"%s", __func__);
  NSLog(@"  * contentSize = %@", NSStringFromCGRect(self.webView.scrollView.contentSize));
}
このテクを使うことで、例えばUIWebView自体をスクロールさせないようにした上で通知を受け取ってUIWebViewのframeをどんどん更新するようにし、UIScrollViewの中に埋め込んでしまってUIImageViewやUILabelのようにして使うみたいなUIを作ることが簡単にできます。HTMLの高さに依存するコードも作れますし、なかなか面白いですよ。iOS 5はもちろん、iOS 4でも動作します。(余談で解説)

■余談
UIWebViewのscrollViewプロパティは実はiOS 4のころからPrivate APIとして存在します。ですがiOS 5よりパブリック扱いになったおかげか、iOS 4向けのアプリでscrollViewプロパティを触っていてもリジェクトされたりクラッシュすることなく普通に使えるので非常に嬉しいです。iOS 3では使えませんのであしからず。

iOS 5でのSSL/TLS通信時にエラーが発生した場合のエラーコードの調べ方

iOS 5より新たにSecurity.frameworkというフレームワークが追加されました。このフレームワークはAppleが実装したSSL/TLS用のライブラリで、iOS 5よりCFNetwork系のクラス(NSURLConnectionなどの内部実装にも使われています)のSSL/TLS通信時に使われるようになったみたいです。

http://developer.apple.com/library/ios/#releasenotes/General/WhatsNewIniPhoneOS/Articles/iOS5.html の一番下にちょこっとだけ説明があるので、見てみると、
Security
The Security framework (Security.framework) now includes the Secure Transport interfaces, which are Apple’s implementation of the SSL/TLS protocols. You can use these interfaces to configure and manage SSL sessions, manage ciphers, and manage certificates.

For information about the Secure Transport interfaces, see the SecureTransport.h header file of the Security framework.
うむ、ドキュメント無いからヘッダ読めとはなかなか適当ですね。

普通にアプリを作っている最中にこのSecurity.frameworkを直接触る必要は全くないのですが、問題はこのSecurity.frameworkを使っているAppleのframeworkが通信時にSSL/TLS絡みのエラーを吐いたときに発生します。このとき、それらのフレームワークは以下のようなNSErrorを返してきます。
Error Domain=NSOSStatusErrorDomain Code=-9830 "The operation couldn’t be completed. (OSStatus error -9830.)
NSErrorの読み方はErrorDomainに指定されている文字列に応じてエラー番号が定義されているドキュメント/ヘッダファイルを見るというものなのですが、このNSErrorはNSOSStatusErrorDomainのErrorDomainを指定しているにも関わらず<CarbonCore/MacErrors.h>にエラー番号の定義が全く書いていないのです!

色々原因を調べた結果、以下のQ&Aが見当たりました。
http://developer.apple.com/library/mac/#qa/qa1499/_index.html
どうやらSecurity.framework絡みのエラーはNSOSStatusErrorDomainを使っているがMacErrors.hではなく<Security/SecureTransport.h>などに定義が書いてあるみたいなのです。見てみると・・・
/*************************************************
 *** OSStatus values unique to SecureTransport ***
 *************************************************/

/*
    Note: the comments that appear after these errors are used to create SecErrorMessages.strings.
    The comments must not be multi-line, and should be in a form meaningful to an end user. If
    a different or additional comment is needed, it can be put in the header doc format, or on a
    line that does not start with errZZZ.
*/

enum {
 errSSLProtocol    = -9800, /* SSL protocol error */
 errSSLNegotiation   = -9801, /* Cipher Suite negotiation failure */
 errSSLFatalAlert   = -9802, /* Fatal alert */
 errSSLWouldBlock   = -9803, /* I/O would block (not fatal) */
    errSSLSessionNotFound   = -9804, /* attempt to restore an unknown session */
    errSSLClosedGraceful   = -9805, /* connection closed gracefully */
    errSSLClosedAbort    = -9806, /* connection closed via error */
    errSSLXCertChainInvalid  = -9807, /* invalid certificate chain */
    errSSLBadCert    = -9808, /* bad certificate format */
 errSSLCrypto    = -9809, /* underlying cryptographic error */
 errSSLInternal    = -9810, /* Internal error */
 errSSLModuleAttach   = -9811, /* module attach failure */
    errSSLUnknownRootCert  = -9812, /* valid cert chain, untrusted root */
    errSSLNoRootCert   = -9813, /* cert chain not verified by root */
 errSSLCertExpired   = -9814, /* chain had an expired cert */
 errSSLCertNotYetValid  = -9815, /* chain had a cert not yet valid */
 errSSLClosedNoNotify  = -9816, /* server closed session with no notification */
 errSSLBufferOverflow  = -9817, /* insufficient buffer provided */
 errSSLBadCipherSuite  = -9818, /* bad SSLCipherSuite */
 
 /* fatal errors detected by peer */
 errSSLPeerUnexpectedMsg  = -9819, /* unexpected message received */
 errSSLPeerBadRecordMac  = -9820, /* bad MAC */
 errSSLPeerDecryptionFail = -9821, /* decryption failed */
 errSSLPeerRecordOverflow = -9822, /* record overflow */
 errSSLPeerDecompressFail = -9823, /* decompression failure */
 errSSLPeerHandshakeFail  = -9824, /* handshake failure */
 errSSLPeerBadCert   = -9825, /* misc. bad certificate */
 errSSLPeerUnsupportedCert = -9826, /* bad unsupported cert format */
 errSSLPeerCertRevoked  = -9827, /* certificate revoked */
 errSSLPeerCertExpired  = -9828, /* certificate expired */
 errSSLPeerCertUnknown  = -9829, /* unknown certificate */
 errSSLIllegalParam   = -9830, /* illegal parameter */
 errSSLPeerUnknownCA   = -9831, /* unknown Cert Authority */
 errSSLPeerAccessDenied  = -9832, /* access denied */
 errSSLPeerDecodeError  = -9833, /* decoding error */
 errSSLPeerDecryptError  = -9834, /* decryption error */
 errSSLPeerExportRestriction = -9835, /* export restriction */
 errSSLPeerProtocolVersion = -9836, /* bad protocol version */
 errSSLPeerInsufficientSecurity = -9837, /* insufficient security */
 errSSLPeerInternalError  = -9838, /* internal error */
 errSSLPeerUserCancelled  = -9839, /* user canceled */
 errSSLPeerNoRenegotiation = -9840, /* no renegotiation allowed */

 /* non-fatal result codes */
 errSSLPeerAuthCompleted     = -9841,    /* peer cert is valid, or was ignored if verification disabled*/
 errSSLClientCertRequested = -9842, /* server has requested a client cert */

 /* more errors detected by us */
 errSSLHostNameMismatch  = -9843, /* peer host name mismatch */
 errSSLConnectionRefused  = -9844, /* peer dropped connection before responding */
 errSSLDecryptionFail  = -9845, /* decryption failure */
 errSSLBadRecordMac   = -9846, /* bad MAC */
 errSSLRecordOverflow  = -9847, /* record overflow */
 errSSLBadConfiguration  = -9848, /* configuration error */
 errSSLLast     = -9849, /* end of range, to be deleted */

    /* DEPRECATED aliases for errSSLPeerAuthCompleted */
    errSSLServerAuthCompleted   = -9841, /* server cert is valid, or was ignored if verification disabled DEPRECATED */
 errSSLClientAuthCompleted   = -9841,    /* client cert is valid, or was ignored if verification disabled; reusing error as you can only be client or server - DEPRECATED */

};
ばっちり書いてますね。これでエラーの原因も特定できます。今回のケースはerrSSLIllegalParamだったみたいですね。

2011年11月18日金曜日

iOS 5の日本語キーボードの高さに対応する (iOS 3, 4, 5全対応)

iOS 5より日本語キーボードの高さが変わっているので、今まで決め打ちで高さ216pxとかやってレイアウトしていたビューが軒並み使えなくなってしまいました。今後はキーボードが出たり引っ込んだり種類が切り替わったりのタイミングできちんとキーボードの大きさを調べて適切にビューをレイアウトしてやる必要があります。ということでその対応をしたのでメモ。

前提条件として、以下の要件を満たすように作りました。
  • iOS 3, 4, 5全てで正常に動作すること。iOS 3.0でも動作しなければならない。
  • キーボードのframeを適切に取得できること
  • キーボードが出てくるタイミング、消えるタイミング、キーボードの種類が変わるタイミング、全て取れること

■まずはログを見てみる
キーボードの動作のタイミング、およびキーボードのframeは、NSNotificationを使って取得することができます。使用するNotification名はUIWindowのドキュメントに以下のように定義されています。
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIWindow_Class/UIWindowClassReference/UIWindowClassReference.html
// Available from iOS 2
UIKeyboardWillShowNotification
UIKeyboardDidShowNotification
UIKeyboardWillHideNotification
UIKeyboardDidHideNotification
// Available from iOS 5
UIKeyboardWillChangeFrameNotification
UIKeyboardDidChangeFrameNotification
で、これらのNotification名でNSNotificationCenterにobserverを追加すると、通知が飛んできます。飛んできたNSNotificationオブジェクトのuserInfoプロパティに特定のキーでキーボードのframeが格納されているというしくみです。使えるキーは以下のとおり。
// Available from iOS 3.2
UIKeyboardFrameBeginUserInfoKey
UIKeyboardFrameEndUserInfoKey
// Available from iOS 3.0
UIKeyboardAnimationCurveUserInfoKey
UIKeyboardAnimationDurationUserInfoKey
// Available from iOS 2.0 ~ 3.2 (Deprecated in newer versions)
UIKeyboardCenterBeginUserInfoKey
UIKeyboardCenterEndUserInfoKey
UIKeyboardBoundsUserInfoKey
iOS 3, 4, 5すべてできちんと動作しなければならないので、これらをふまえて、以下のように実装します。
  • Notification名にはUIKeyboardWillShowNotification, UIKeyboardDidShowNotification, UIKeyboardWillHideNotification, UIKeyboardDidHideNotificationを使う。
  • userInfoのキーには、iOS 3.0/3.1のみUIKeyboardBoundsUserInfoKeyを使い、それ以外のバージョンではUIKeyboardFrameEndUserInfoKeyを使う。

■実装してみる
ということでまずはサンプルアプリを作って動かしてみて、実際に動作を見てみることにしました。大体こんな感じのコードです。
- (void)viewWillAppear:(BOOL)animated
{
  // Notification observerを追加する
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardDidShowNotification:) name:UIKeyboardDidShowNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardDidHideNotification:) name:UIKeyboardDidHideNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
  // Notification observerを削除する
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)onUIKeyboardWillShowNotification:(NSNotification *)notification
{
  NSLog(@"%s", __func__);
  NSLog(@"  * userInfo = %@", notification.userInfo);
  // UIKeyboardFrameEndUserInfoKeyが使える時と使えない時で処理を分ける
  CGRect bounds;
  if (&UIKeyboardFrameEndUserInfoKey == NULL) {
    // iOS 3.0 or 3.1
    // bounds
    bounds = [[notification.userInfo objectForKey:UIKeyboardBoundsUserInfoKey] CGRectValue];
  } else {
    // それ以外
    // frameだがoriginを使わないのでbounds扱いで良い
    bounds = [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
  }
  // boundsを使った処理をここに書く
}

ログはこんな感じに。

iOS 5.0, iPhone 4S
2011-10-19 10:35:15.007 SampleApp[5675:707] -[SampleViewController viewWillAppear:]
// 前の画面の英字キーボードが一旦引っ込んで出てきている
2011-10-19 10:35:15.502 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:35:15.506 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
2011-10-19 10:35:15.509 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:15.510 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.35";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:15.513 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:15.514 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.35";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:16.162 SampleApp[5675:707] -[SampleViewController viewDidAppear:]
// キーボード引っ込める
2011-10-19 10:35:44.246 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:35:44.247 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
2011-10-19 10:35:44.509 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:35:44.511 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
// キーボード出す
2011-10-19 10:35:55.135 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:55.136 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:55.397 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:55.398 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
// 日本語キーボードに変更
2011-10-19 10:35:58.167 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:58.168 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0;
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 390}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 354}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
}
2011-10-19 10:35:58.170 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:58.171 SampleApp[5675:707]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 390}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 354}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
}
// 英語キーボードに変更
2011-10-19 10:36:00.483 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:36:00.484 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0;
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 336}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:36:00.485 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:36:00.486 SampleApp[5675:707]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 336}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
// 画面を抜ける、キーボードが隠れる通知より先にviewWillDissappearされるので通知が来ない
2011-10-19 10:36:03.570 SampleApp[5675:707] -[SampleViewController viewWillDisappear:]
2011-10-19 10:36:03.986 SampleApp[5675:707] -[SampleViewController viewDidDisappear:]
iOS 3.1.3, iPhone 3G
2011-10-19 10:43:46.548 SampleApp[352:207] -[SampleViewController viewWillAppear:]
// 前の画面の英字キーボードが一旦引っ込んで出てきているのだが、iOS 3.1.3では引っ込む側の挙動が見られない。出てくるだけになっているようだ。
// さらにDidShowの通知がキーボードがviewDidAppearの呼び出しのあとに行われるようになっている。
// どうやらキーボードがどこに属しているのかが違うみたいだな。
2011-10-19 10:43:47.202 SampleApp[352:207] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:43:47.210 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.35;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {480, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
2011-10-19 10:43:48.253 SampleApp[352:207] -[SampleViewController viewDidAppear:]
2011-10-19 10:43:48.315 SampleApp[352:207] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:43:48.336 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
// キーボード隠す
2011-10-19 10:43:52.854 SampleApp[352:207] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:43:52.862 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.300000011920929;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 588};
}
2011-10-19 10:43:53.183 SampleApp[352:207] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:43:53.188 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 588};
}
// キーボード出す
2011-10-19 10:43:54.621 SampleApp[352:207] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:43:54.629 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.300000011920929;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
2011-10-19 10:43:54.972 SampleApp[352:207] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:43:54.977 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
// キーボードの種類をここで英語→日本語、日本語→英語に変えているのだが、通知が来ない
// だがiOS 3ではキーボードの種類によって高さが違うということが(基本)ないので気にしなくて良い
[Switching to process 11779 thread 0x2e03]
warning: No copy of  found locally, reading from memory on remote device.  This may slow down the debug session.
// 画面抜ける、viewWillDisappearより先にKeyboardが隠れる通知が来る
2011-10-19 10:44:07.442 SampleApp[352:207] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:44:07.454 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.35;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {480, 372};
}
2011-10-19 10:44:07.472 SampleApp[352:207] -[SampleViewController viewWillDisappear:]
2011-10-19 10:44:08.374 SampleApp[352:207] -[SampleViewController viewDidDisappear:]
問題なさそうですね。キーボードの種類が切り替わったタイミングにもUIKeyboardWillShowNotificationとUIKeyboardWillHideNotificationがきちんと呼ばれているようです。これならUIKeyboardWillChangeFrameNotificationを使う必要はあんまり無いように思えます。実際、UIKeyboardWillChangeFrameNotificationを使わないでUIKeyboardWillShowNotificationとUIKeyboardWillHideNotificationだけを使ったアプリをリリースしていますが、特に問題なさそうです。

2011年10月10日月曜日

iOS で Private API を使って Bluetooth Keyboard の状態を取ったりしたいメモ

iOS で Private API を使って Bluetooth Keyboard の状態を取ったりしたいメモです。言うまでもありませんが以下の参考資料に使われているコードとかを使ったアプリが App Store の審査に通ることは絶対にありません。真似しないでね!
あと以下のコードを使って実機で試したコードがまだないので、Jailbrake していない iOS 4.3.5 で動くかどうかすらわかりません。すみません><

GSEventを使ってKeyboardの修飾キーの状態を得る
https://gist.github.com/1242475

iPhoneのイヤフォンマイクやBluetoothキーボードでシャッターを切れるようにするiRemoteShutter
http://hitoriblog.com/?p=1747

iSSHのBluetoothキーボード対応を強化するiSSHPatcher
http://hitoriblog.com/?p=1798

BluetoothキーボードからiPhone/iPadのタスクスイッチを可能にするAltTab
http://hitoriblog.com/?p=3958

BluetoothManager
http://stackoverflow.com/questions/1743610/programmatically-turn-on-bluetooth-in-the-iphone-sdk
BluetoothDeviceを使えばデバイスの電池残量とかも見られそう

2011年9月18日日曜日

iOS, Android, Windows Phoneのメモリ管理とかメッセージングの仕方を調べてみた

ぼちぼちTwitterにつぶやいていたらTogetterにまとめてくださった方がいらっしゃるので、せっかくなのでこちらでも紹介したいと思います。

http://togetter.com/li/189550 (githubみたいにembedできればいいのになーなんて><)

結構気合い入れて調べたのでおすすめです。

2011年9月12日月曜日

UIWebView の Private API を使って BASIC認証のあるページにアクセスする

元ネタはこちら: http://d.hatena.ne.jp/KishikawaKatsumi/20090603/1243968707

仕事でどうしても以下の要件を満たすUIWebViewが必要になったので作りました。
  • 開発環境にBASIC認証がかかっており、そこにUIWebViewでアクセスしたい。
  • アクセス先のHTMLにリンクが埋め込まれているため、URLをhttp://username:password@example.comのように変換することができない。webView:shouldStartLoadWithRequest:navigationType:で頑張ればいける気がしなくともないのですが結局断念しました。
  • 諸事情によりNSURLConnectionが使えない(当然ASIもダメ。あくまでUIWebViewでアクセスする必要がある)。
普通につくるとどうにもうまくいかなかったので、結局UIWebViewをオーバーライドしてPrivate APIを叩く作戦を取ることにしました。

というわけで出来上がったソースコードはこちら。MITライセンスです。
https://gist.github.com/1210372
iOS 4.3.5で動作確認しています。iOS 5でも多分動作します。 iOS 3以下はわかりません><

注意: このコードにはPrivate APIが多分に含まれています。このコードが含まれたアプリをApp Storeに提出しても十中八九審査に通りません。あくまで開発環境での検証用または自分のiPhoneに入れてニヤニヤするコードにのみお使いください。


■しくみ

UIKitをclass-dumpしてたら使えそうなシグネチャを見つけたので、継承してオーバーライドしてゴニョゴニョしてたらできちゃった!って感じです。それにしてもWebKitはPrivate APIにしておくにはもったいない出来の良さですね。このAPIへのフルアクセスがあればAndroidのWebViewと同等かそれ以上に自由に使えるのですが。

2011年8月22日月曜日

UDIDが使えなくなりそうなので、UIIDを使えるようにしました

■2012/11/11追記
iOS 6より[[UIDevice currentDevice] identifierForVendor]というAPIがAppleより提供され、よりプライバシーに配慮した上により安全な方法で自分の開発したアプリケーションを利用するユーザーを個別に認証することが可能になりました。それに伴い拙作のライブラリもidentifierForVendorが利用可能であればこちらを利用するように修正いたしました。今後はこのidentifierForVendor(または広告APIなどを作る場合であれば[[UIDevice sharedManager] advertisingIdentifier])が個体認識の主流になっていくと思われます。identifierForVendorとadvertisingIdentifierの仕様まとめは http://stackoverflow.com/questions/11836225/ios6-udid-what-advantages-does-identifierforvendor-have-over-identifierforadve が一番詳細かなと思います。
追記終


つい先日TechCrunchがiOS5よりUDIDの使用が非推奨になると報道し、巷はiOSでのUDIDの使用についての話題で俄然盛り上がっています。セキュリティ的によろしくないから良い変更だという声もあれば、すでに認証用として使っていてシステム改修が必要という悲観の声もあります。しかし私はどちらかというとUDIDをバリバリ使っちゃってる方陣営の人なので、セキュリティの問題については知っていても、やはりUDID相当の物が無いと不便だなぁと思ってしまうのです。そこでプログラマらしくコードで解決することにしました。

UDIDにはセキュリティの懸念があるし、もう使えなくなる。だったらもっといいIDを超簡単に生成して使えるようにすればいいじゃない。ということでUnique Installation Identifier (インストール毎ID、UIID) を生成するライブラリを早速書いてみました。
ソースコードはこちら。MITライセンスです。
https://github.com/akisute/UIApplication-UIID
昔のgistはこちら

使い方は超簡単で、
  1. アプリが一度削除されたとしても同一のUIIDを返すようにしたいのであれば、#define UIID_PERSISTENT=1して、Security.frameworkをプロジェクトに追加する
  2. UIApplication+UIID.hをimport
  3. [[UIApplication sharedApplication] uniqueInstallationIdentifier]で取れます
はいこれだけです。


■そもそもどうしてUDIDを使うのか

たいていの場合は以下のような理由です。
  • ログイン機構なしに簡易にユーザーの識別をしたいときに、毎回同じ値を返し、かつユーザー事に異なる値が取得出来る何かが必要になるので、UDIDを使う
  • UDID値の取得がいつでもどこでも可能、さらに超簡単で外部ライブラリのインストールなど面倒なことが一切無い
  • しかもAppleのドキュメントにそう使えって書いてある。・・・補足すると使ってもいいよ、ぐらいのニュアンスで、使えという風に推奨はしてないみたいです>< またUDIDのみをキーとしてユーザー情報を保存するな、とも書いてますね。


■UDIDは何がまずいの

主にまずい理由はセキュリティです。特にガラケーのかんたんログインの問題が参考になるのですが、iOSの場合は以下のようなセキュリティ問題が発生します。
  • 相手のUDIDがわかってしまえば、簡単になりすましが可能
  • UDIDの値を返すメソッドの実装を差し替えて、任意の値を返すように出来る。JailBreakしているユーザーであれば誰でも簡単に実行可能、そのためのアプリもCydiaで検索すれば転がっている
  • UDID自体、様々な方法で取得可能。アプリをインストールさせてそこ経由で集めてみたりはもちろん(その際値を取得することに対する警告すら出ない)、iTunesからでも値を確認できる
  • 上記三つの理由のため、その気になればたやすく任意のユーザーになりすませる
  • さらに恐ろしいことにUDIDは端末事に一意になるため、一度値が特定されてしまうと端末を買い換えない限りずっとなりすまされてしまう、リセットできない


■UIIDだとどうなる

Unique Installation Identifier, UIIDはUDIDと異なり「アプリのインストール基盤毎」に一意な値を返すような実装になっています。簡単にまとめると、あるアプリAが、デバイス1, 2, 3にインストールされた場合、UIIDは1, 2, 3の全てで異なる値になります。ここまではUDIDと同じですが、UDIDと違うのはデバイス1に別々のアプリA, B, Cがインストールされた場合、それぞれ異なる値になります。中身は単にCFUUIDというiOSに元からある良くできたID生成ルーチンなんですが、これにより以下のようなメリットが得られます。
  • 外部からIDの値を取得するのが困難。推測も十分に長い上に独立な値なため困難、取得も(UIID_PERSISTENT=1でビルドすればKeychainを使うので)困難です。
  • 万が一何かの間違いで流出したとしても、アプリごとに異なる値になるため他のアプリのセキュリティが犯されたりはしない。また同様の理由で他の悪意のあるアプリからUIIDを取得して攻撃することもできない。
  • プログラム的にUIIDをリセット可能。UUID_PERSISTENT=0であればアプリを消せばユーザーが任意にリセット可能。
  • Keychainを使ったメリットとして、ユーザーがデバイスを乗り換えてもiTunesのバックアップに値が保持されるため、それから復元を行った場合UIIDの値が引き継がれる。
  • 通常のアプリであればUDIDを使っていたケースのほとんどはこのUIIDでそのまま代用可能、あとは過去のUDIDとUIIDのヒモ付だけサーバー側でやってしまえば完全に乗り換えられる
  • それでもどうしてもデバイスごとに一意な値が欲しいならhttps://github.com/gekitz/UIDevice-with-UniqueIdentifier-for-iOS-5/blob/master/Classes/UIDevice+IdentifierAddition.mなど使えばよいのかなと。ただしこの実装はMACアドレスの値を使っているため、MACアドレスの値がわかってしまえばIDをたやすく生成可能で、UDIDの持つセキュリティ上の問題は残ります。


■使用上の注意

UDIDよりは問題が少ないですが、それでもこのUIIDだけで認証を行うような作りのアプリには間違ってもしないように!以下のような問題があります。
  • そもそもこの値は完全にユーザーと一意にヒモ付いた値ではありません。UIIDはあくまでアプリのインストール単位とヒモ付いているだけです。複数のデバイスを一人のユーザーが所有していたりすると完全にアウトですし、インストールされた端末が譲与された場合も対応できません。
  • プログラム的にUIIDはリセット可能なので最悪の場合でもずっとなりすまされるのは回避できるのですが、その際にヒモ付けられていたユーザー固有の情報が消えてしまいます。ログインIDとパスワードで認証をしているのであれば、ログインIDはそのままにしてパスワードだけをリセットすることで、なりすましの問題を解決しつつ、ユーザーの一意性は保ったままにできるため、万が一の際はログインIDとパスワードを使っている方が圧倒的に利便性が高まります。
  • 以上の理由により、UIIDはあくまで簡易的・一時的にユーザー認証をする時に使用し(たとえばユーザーが購入したアプリ内課金の商品をアプリが消されるまでの間だけ履歴として持っておいたり、ゲームのランキング等で匿名だけれど点数ランキングに参加できるようにしたり)、正式で完全なサービスはログインIDとパスワードによって提供するべきです。そうすることで一人のユーザーが複数のデバイスでサービスを使えるようになります。さらにはAndroidとも連携できたりしますしお得です。


■っていうか

全部高木先生が一年前に言っている通りになっちゃってるじゃないすか!っていうか私が作った物もこの高木先生がおっしゃってる「アプリ専用の(セキュアな)独自IDを生成してそれを保存して使う」というものの実装にすぎません。しかしまったく、せっかく警告してもらっても、人間実際に問題に直面しなければなかなか手をつけないものですね><

しかし、しかしですね、あえてここで一言、エンジニアとして申させていただきたい。

私、エンジニアが欲しいのはセキュリティ上正しい実装の方法だとか、概念だとか、ましてやどこそこのログイン方法はいけてないから直せや、などという文章でもないのです。我々が欲しいのは、「すでに実装されている、セキュリティ上正しくて、猿でも理解できて、1分で組み込めて、どのような環境でも動き、ユーザーが会員登録なんて面倒極まりないことをしなくても済むユーザー認証の手段」なんです。要するに、
// 何か良くわからんけどこのトークンをHTTPS経由でPOSTして認証しておけば超スーパー確実かつセキュアで猿でも実装できてハッピーになる
[[NSAuthentication sharedUser] authenticationToken]
↑コレが今すぐ欲しいんですよ、我々エンジニアというのは。そうすれば誰だってUDIDを使って認証するみたいな面倒くさいことするわけ無いじゃないですか。頼まれたってやりませんよ。

私はユーザー認証をしたいだけなんです、それも可能な限り楽に。口で何と批判しようが、正しい方法を教えようが、世の中は決して変わらないと思います。みんな楽をしたいから。なのでAppleには是非UDIDを廃止するこの機会にぜひ上記のような何かをUIKitなりFoundationなりに組み込んで欲しいですね。こういうところも、良いプラットフォーマーの責務の一部じゃないかなと。