2011年11月27日日曜日
Cocoa Framework に用意されていないロックを Objective-C で実装する
Cocoaフレームワークは非同期処理時のロックを取るために、NSLockingというプロトコルと、NSLock, NSRecurrsiveLock, そしてNSConditionalLockという3種類のロックの実装を提供しています。が、残念ながらちょっとまともな非同期コードを書こうと思うとこれでは全然足りません。っていうか、NSConditionalLockがロック抜けるときにしか条件値を書き換えられない実装なのが正直いけてないと思います。これじゃCounting Lock(最初に決めた数だけ同時にロックできるロック。Counting Semaphoreともいう)にもRead/Write Lock(書き込みロックと読み込みロックの二種類が用意され、書き込みロックが取られていない限りは、何個でも同時に読み込みロックが取れる、効率のいいロック)にも使えません。というわけで、Objective-Cで書かれたCounting LockとRead/Write Lockを見つけましたのでご紹介いたします。
http://cocoaheads.byu.edu/wiki/locks
中身はpthread.hのpthread_mutexを使って実装しているようです。一見危なそうですが、Cocoaフレームワークが使用するスレッドは全て内部実装がpthreadなので全く問題ありません。
2011年11月23日水曜日
静的ライブラリ中のシグネチャが衝突してビルドできないときに再ビルドしないでシグネチャを書き換える
皆さんも以下のようなビルドエラーを見たことが一度はあると思います。
これはビルド時に同一プロジェクト内に同じ名前のシグネチャの関数やクラスが存在するためリンクができなくて失敗しているというエラーです。特に以下のようなケースでよく発生します。
■具体例
AdMobのSDK(libGoogleAdMob.a)とopenssl(libcrypto.a)を同時に一つのプロジェクトにインストールした時、冒頭の画像のようにMD5というシグネチャがビルド時に衝突してしまうのです。AdMobのはプロプロエタリなので当然書き換えられませんし、opensslのように巨大で複雑なコードに手を入れて再ビルドするのも非常に危険です。そもそもopensslはビルド自体が難しいのです。
このような場合は、ソースコード自体を書き換えるのではなくビルド済みの静的ライブラリ側のオブジェクトファイルを書き換えることで対処を行うことが可能です。
コンパイル時のリンカの設定を変更すれば対処できそうな気もするんですが、GNU ldにはそのようなオプションが見当たりませんでした。なんかSun Solarisのldだと対処できるみたいです。
参考: http://stackoverflow.com/questions/6940384/how-to-deal-with-symbol-collisions-between-statically-linked-libraries
参考: http://stackoverflow.com/questions/393980/restricting-symbols-in-a-linux-static-library
注意: 以下の手順は失敗すると静的ライブラリ自体が完全に破損したり、実行時に深刻な問題が発生する可能性があります!! 以下の解説を見てもちんぷんかんぷんな人は適用しないことを強くおすすめいたします。この手順を適用したことによって発生したいかなる損害についても私は責任をとりかねます。
■静的ライブラリ内部のシグネチャを書き換える方法
静的ライブラリ内部のシグネチャを書き換えるには、以下のようなツールを使用します。
lipo
以前にもご紹介した、Apple純正のユニバーサルバイナリ/ユニバーサルライブラリ(fat binaryともいいます)を作ったりバラしたりするツールです。iOSのライブラリはほぼすべてがi386, armv6, armv7の三種類に対応するfat binaryになっており、基本的にApple純正でないツールはそういったfat binaryに対して歯が立たないので、まずこのツールを使って普通のライブラリに戻した上で、以下のツールを使うわけです。
nm
こいつもApple純正です。バイナリの中に入っているシグネチャの名前を一覧表示することができます。Apple純正なのでfat binaryに対しても使えて超便利です。
objdump
GNU objdumpというツールがありまして、こいつを使うとバイナリの詳細な中身を覗き見ることができます。nmよりも表示される情報が詳細です。Apple純正ではないので、以下で紹介されているようにしてMacPorts経由でインストールするのをお勧めします。
http://d.hatena.ne.jp/amachang/20080401/1207027290
objcopy
GNU objcopyです。ライブラリ内部のシグネチャを書き換える事ができるツールで、objdumpとセットでついてくるのですが、残念ながらiOS向けのバイナリに対しては全く使えません。話すと長くなるのですがバージョンを変えようがarm向けのセットをインストールしようがarでライブラリからオブジェクトファイルを取り出して試みてみようが何やっても一切無駄です。使えませんので諦めてください。
参考: http://www.mail-archive.com/bug-binutils@gnu.org/msg02829.html
参考: http://stackoverflow.com/questions/2231698/how-can-i-easily-install-arm-elf-gcc-on-os-x
objconv
で、使えないobjcopyに代わって、今回の英雄です。こいつを使って実際にライブラリ内部のシグネチャを自由自在に書き換えることができます。メンテもしっかり行われているようで動作も安定しています。以下のサイトからダウンロードできます。
サイト: http://www.agner.org/optimize/#objconv
マニュアル: http://www.agner.org/optimize/objconv-instructions.pdf
残念ながらsource配布のみしか無いため、自分でビルドしてやる必要があります。と言ってもそこそこ簡単で、以下のようにするだけです。
■実践:全く同一のライブラリのシグネチャだった場合
base64なんかでよく発生します。この場合は片方をweakシンボルにします。
weakシンボルとは: http://d.hatena.ne.jp/syohex/20100610/1276180481 がわかりやすいです。
早速やってみましょう。以下の例ではlibTest.a中のbase64_encodeシグネチャを書き換えます。
まずは以下のコマンドで対象のライブラリのfat binaryを通常のバイナリに戻して:
■実践:同じ名前の違うライブラリのシグネチャだった場合
冒頭のMD5のケースがこれです。名前が同じなのに実装がまるで違うので、weakシンボルにすると深刻なバグが発生します。こういう時は慎重に見定めた上で、使われていないと思われる方のシグネチャをhiddenシンボル(ローカルシンボル)にして、外部ファイルからリンクできないようにしてしまいます。これなら実装は存在しますがリンクされないようになるだけなので、対象のシンボルが外部から使われていないのであればこれだけでいけます。
今度はlibcrypto.a中の_MD5シグネチャを書き換えてみましょう。
こちらもまずはlipoを使って通常のバイナリに戻して:
■実践:同じ名前の違うライブラリのシグネチャで、かつ外からバリバリ呼ばれていた場合
これはビルド時に同一プロジェクト内に同じ名前のシグネチャの関数やクラスが存在するためリンクができなくて失敗しているというエラーです。特に以下のようなケースでよく発生します。
- 自分が作ったクラスや関数の名前と、外部から持ってきたライブラリが使っているクラスや関数の名前が衝突している
- 外部から持ってきたライブラリ同士でクラスや関数の名前が衝突している
- 外部ライブラリをインストールする際に、-all_loadしたり-ObjCしたりている
■具体例
AdMobのSDK(libGoogleAdMob.a)とopenssl(libcrypto.a)を同時に一つのプロジェクトにインストールした時、冒頭の画像のようにMD5というシグネチャがビルド時に衝突してしまうのです。AdMobのはプロプロエタリなので当然書き換えられませんし、opensslのように巨大で複雑なコードに手を入れて再ビルドするのも非常に危険です。そもそもopensslはビルド自体が難しいのです。
このような場合は、ソースコード自体を書き換えるのではなくビルド済みの静的ライブラリ側のオブジェクトファイルを書き換えることで対処を行うことが可能です。
コンパイル時のリンカの設定を変更すれば対処できそうな気もするんですが、GNU ldにはそのようなオプションが見当たりませんでした。なんかSun Solarisのldだと対処できるみたいです。
参考: http://stackoverflow.com/questions/6940384/how-to-deal-with-symbol-collisions-between-statically-linked-libraries
参考: http://stackoverflow.com/questions/393980/restricting-symbols-in-a-linux-static-library
注意: 以下の手順は失敗すると静的ライブラリ自体が完全に破損したり、実行時に深刻な問題が発生する可能性があります!! 以下の解説を見てもちんぷんかんぷんな人は適用しないことを強くおすすめいたします。この手順を適用したことによって発生したいかなる損害についても私は責任をとりかねます。
■静的ライブラリ内部のシグネチャを書き換える方法
静的ライブラリ内部のシグネチャを書き換えるには、以下のようなツールを使用します。
lipo
以前にもご紹介した、Apple純正のユニバーサルバイナリ/ユニバーサルライブラリ(fat binaryともいいます)を作ったりバラしたりするツールです。iOSのライブラリはほぼすべてがi386, armv6, armv7の三種類に対応するfat binaryになっており、基本的にApple純正でないツールはそういったfat binaryに対して歯が立たないので、まずこのツールを使って普通のライブラリに戻した上で、以下のツールを使うわけです。
nm
こいつもApple純正です。バイナリの中に入っているシグネチャの名前を一覧表示することができます。Apple純正なのでfat binaryに対しても使えて超便利です。
objdump
GNU objdumpというツールがありまして、こいつを使うとバイナリの詳細な中身を覗き見ることができます。nmよりも表示される情報が詳細です。Apple純正ではないので、以下で紹介されているようにしてMacPorts経由でインストールするのをお勧めします。
http://d.hatena.ne.jp/amachang/20080401/1207027290
objcopy
GNU objcopyです。ライブラリ内部のシグネチャを書き換える事ができるツールで、objdumpとセットでついてくるのですが、残念ながらiOS向けのバイナリに対しては全く使えません。話すと長くなるのですがバージョンを変えようがarm向けのセットをインストールしようがarでライブラリからオブジェクトファイルを取り出して試みてみようが何やっても一切無駄です。使えませんので諦めてください。
参考: http://www.mail-archive.com/bug-binutils@gnu.org/msg02829.html
参考: http://stackoverflow.com/questions/2231698/how-can-i-easily-install-arm-elf-gcc-on-os-x
objconv
で、使えないobjcopyに代わって、今回の英雄です。こいつを使って実際にライブラリ内部のシグネチャを自由自在に書き換えることができます。メンテもしっかり行われているようで動作も安定しています。以下のサイトからダウンロードできます。
サイト: http://www.agner.org/optimize/#objconv
マニュアル: http://www.agner.org/optimize/objconv-instructions.pdf
残念ながらsource配布のみしか無いため、自分でビルドしてやる必要があります。と言ってもそこそこ簡単で、以下のようにするだけです。
- ソースコードをダウンロードする
- zipを解凍する
- source.zipを解凍する
- build.shを実行する。ただしこのシェルスクリプトは1行目だけがひどくバグってるので、自分で中を見てビルドコマンドを叩いてやるようにしてください。ね?簡単でしょう?
■実践:全く同一のライブラリのシグネチャだった場合
base64なんかでよく発生します。この場合は片方をweakシンボルにします。
weakシンボルとは: http://d.hatena.ne.jp/syohex/20100610/1276180481 がわかりやすいです。
早速やってみましょう。以下の例ではlibTest.a中のbase64_encodeシグネチャを書き換えます。
まずは以下のコマンドで対象のライブラリのfat binaryを通常のバイナリに戻して:
lipo -thin armv6 libTest.a -output libTest_armv6.a lipo -thin armv7 libTest.a -output libTest_armv7.a lipo -thin i386 libTest.a -output libTest_i386.aobjconvを実行:
objconv -fmacho -nw:base64_encode libTest_armv6.a objconv -fmacho -nw:base64_encode libTest_armv7.a objconv -fmacho -nw:base64_encode libTest_i386.alipoで元通りに戻します:
lipo -create libTest_i386.a libTest_armv6.a libTest_armv7.a -output libTest.aこれでビルドが通るはずです。
■実践:同じ名前の違うライブラリのシグネチャだった場合
冒頭のMD5のケースがこれです。名前が同じなのに実装がまるで違うので、weakシンボルにすると深刻なバグが発生します。こういう時は慎重に見定めた上で、使われていないと思われる方のシグネチャをhiddenシンボル(ローカルシンボル)にして、外部ファイルからリンクできないようにしてしまいます。これなら実装は存在しますがリンクされないようになるだけなので、対象のシンボルが外部から使われていないのであればこれだけでいけます。
今度はlibcrypto.a中の_MD5シグネチャを書き換えてみましょう。
こちらもまずはlipoを使って通常のバイナリに戻して:
lipo -thin armv6 libcrypto.a -output libcrypto_armv6.a lipo -thin armv7 libcrypto.a -output libcrypto_armv7.a lipo -thin i386 libcrypto.a -output libcrypto_i386.aobjconvを実行:
objconv -fmacho -nl:_MD5 libcrypto_armv6.a objconv -fmacho -nl:_MD5 libcrypto_armv7.a objconv -fmacho -nl:_MD5 libcrypto_i386.alipoで元通りにして完成:
lipo -create libcrypto_i386.a libcrypto_armv6.a libcrypto_armv7.a -output libcrypto.aこれで無事ビルドが通りました。
■実践:同じ名前の違うライブラリのシグネチャで、かつ外からバリバリ呼ばれていた場合
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)が変わったタイミングで通知を受け取ることができます。こんなコードになります。
■余談
UIWebViewのscrollViewプロパティは実はiOS 4のころからPrivate APIとして存在します。ですがiOS 5よりパブリック扱いになったおかげか、iOS 4向けのアプリでscrollViewプロパティを触っていてもリジェクトされたりクラッシュすることなく普通に使えるので非常に嬉しいです。iOS 3では使えませんのであしからず。
■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.frameworkを直接触る必要は全くないのですが、問題はこのSecurity.frameworkを使っているAppleのframeworkが通信時にSSL/TLS絡みのエラーを吐いたときに発生します。このとき、それらのフレームワークは以下のようなNSErrorを返してきます。
色々原因を調べた結果、以下のQ&Aが見当たりました。
http://developer.apple.com/library/mac/#qa/qa1499/_index.html
どうやらSecurity.framework絡みのエラーはNSOSStatusErrorDomainを使っているがMacErrors.hではなく<Security/SecureTransport.h>などに定義が書いてあるみたいなのです。見てみると・・・
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とかやってレイアウトしていたビューが軒並み使えなくなってしまいました。今後はキーボードが出たり引っ込んだり種類が切り替わったりのタイミングできちんとキーボードの大きさを調べて適切にビューをレイアウトしてやる必要があります。ということでその対応をしたのでメモ。
前提条件として、以下の要件を満たすように作りました。
■まずはログを見てみる
キーボードの動作のタイミング、およびキーボードのframeは、NSNotificationを使って取得することができます。使用するNotification名はUIWindowのドキュメントに以下のように定義されています。
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIWindow_Class/UIWindowClassReference/UIWindowClassReference.html
■実装してみる
ということでまずはサンプルアプリを作って動かしてみて、実際に動作を見てみることにしました。大体こんな感じのコードです。
ログはこんな感じに。
iOS 5.0, iPhone 4S
前提条件として、以下の要件を満たすように作りました。
- 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 UIKeyboardBoundsUserInfoKeyiOS 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問題なさそうですね。キーボードの種類が切り替わったタイミングにもUIKeyboardWillShowNotificationとUIKeyboardWillHideNotificationがきちんと呼ばれているようです。これならUIKeyboardWillChangeFrameNotificationを使う必要はあんまり無いように思えます。実際、UIKeyboardWillChangeFrameNotificationを使わないでUIKeyboardWillShowNotificationとUIKeyboardWillHideNotificationだけを使ったアプリをリリースしていますが、特に問題なさそうです。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:]