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だけを使ったアプリをリリースしていますが、特に問題なさそうです。