ソフトウェアキーボードのinputAccessoryViewは、高さが可変だと辛い

--

## inputAccessoryViewとは？
ソフトウェアキーボードの上につけられる、カスタマイズ可能なViewです。キーボード自体をいじれないので、オプションのボタンを設置したりするときに便利ですよね。キーボードが消えるときにも一緒に消えてくれるし。
ただ、そのビューの高さが固定なら全然ラクチンなんですが、もし可変の場合（任意数のボタンが出るとか、さらにはキーボード操作中にボタンの数が増減するとか）、とたんにめんどくさいことになります。
「画面内の上xxxピクセルはテキストビューの領域として確保しよう」って思ったとしても、そのxxxピクセルをどう求めるのか？そして、求めた値をどのタイミングでsetFrame:するのか？というのが、かなり大変なのです。
この問題について、当記事では語ろうと思います。


便宜上、以下のように呼びます。

- iav = inputAccessoryView。キーボードの上に補助的に表示できるやつ
- キーボード = 見慣れたソフトウェアキーボード
- コンテナ = iavとキーボードを含めたもの
- iavFrameDefault = iavに設定されているframe
- iavFrameActual = iavに本当に設定したいframe
- kbFrame = キーボードのframe
- containerFrame = コンテナのframe

## キーボード出現の流れ
0. UITextView, UITextFieldなどがfirstResponderとなり、ソフトウェアキーボードを要求される
0. kbFrame、iavFrameDefaultをもとに、containerFrame(位置,サイズ)を決定
0. 【通知】UIKeyboardWillChangeFrameNotification
0. 【通知】UIKeyboardWillShowNotification
0. containerFrameまでアニメーション (UIKeyboardAnimationDurationUserInfoKey > 0)
0. 【通知】UIKeyboardDidChangeFrameNotification
0. 【通知】UIKeyboardDidShowNotification


## 問題
- iavFrameActualはkbFrameがわからないと算出できないが、kbFrameはUIKeyboard***Notification通知が来ないと取得できない
- 通知を受けてiavFrameActualを設定すると、OS側がiavFrameDefaultを元に算出したcontainerFrameと実際のframeの間に差異が生じ、コンテナのレイアウトがズレる。
 具体的には、以下のようなズレ方になる。
 - UIKeyboardWillChangeFrameNotification を受けて設定した場合、iav領域がiavFrameDefaultのサイズしか用意されず、iavが見切れたりする
 - UIKeyboardWillShowNotification を受けて設定した場合、iav領域はiavFrameDefaultではなくiavFrameActual分用意されるが、その差分はcontainerFrame.originには反映されず、キーボードの位置がズレて見切れたりする
 - UIKeyboardDidChangeFrameNotification / UIKeyboardDidShowNotification を受けて設定した場合、レイアウトはiavもキーボードも正しく設定されるものの、通知のタイミングがキーボード出現アニメーションより後なので、アニメーション中のiav領域のサイズはiavFrameDefaultになっており、見た目が悪い


## 解決策
### その1：iavの領域を固定とする
iavのframeは徹頭徹尾、[UIScreen mainScreen].boundsとする。
ただし、iav.userInteractionEnabledをYESにすると、下に表示されているビューがタップできない。
かといって、iav.userInteractionEnabledをNOにするとsubviewsまでタップできなくなってしまう。
そこらへんはhitTest:とか使ってなんとかする。
具体的なコードは書いてないけど、うまくいくと思われる。

### その2：UIKeyboardDidShowNotification のタイミングに合わせて、iavを下からニュッと出すアニメーションをさせる

こっちで自分は解決した。


```objc
@interface TSInputAccessoryView()
{
    CGSize _keyboardSize;
    BOOL _isKeyboardShown;
}
@end

@implementation TSInputAccessoryView

- (id)init
{
    self = [super init];
    if (self) {
        _keyboardSize = CGSizeZero;
        
        self.clipsToBounds = YES;

        //ここポイント！アンカーポイントを下に設定しないと、iavが中空から現れる妙なアニメーションになる
        self.layer.anchorPoint = CGPointMake(0.5, 1.0);
        
        __weak typeof(self) weakSelf = self;

        [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardDidShowNotification
                                                          object:nil
                                                           queue:[NSOperationQueue mainQueue]
                                                      usingBlock:^(NSNotification *note) {
                                                          LOG(@"%@", [note description]);
                                                          NSDictionary *userInfo = note.userInfo;
                                                          
                                                          NSValue* value = [userInfo objectForKey:UIKeyboardFrameEndUserInfoKey];
                                                          
                                                          CGFloat keyboardHeight = [value CGRectValue].size.height-weakSelf.bounds.size.height;
                                                          NSTimeInterval duration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
                                                          
                                                          LOG(@": duration(%f), %@",duration, [value description]);
                                                          if (keyboardHeight == 0) {
                                                              //キーボードが隠れる時に呼ばれた。処理不要なのでスルー
                                                              _isKeyboardShown = NO;
                                                              return;
                                                          } else {
                                                              _isKeyboardShown = YES;
                                                          }
                                                          
                                                          if (duration == 0 && _keyboardSize.height == keyboardHeight) {
                                                              //inputAccessoryViewのフレーム変更。キーボードサイズの変更は不要。
                                                              return;
                                                          }
                                                          
                                                          [weakSelf resizeWithKeyboardRect:value duration:duration/2];
                                                      }];
        [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardDidHideNotification
                                                          object:nil
                                                           queue:[NSOperationQueue mainQueue]
                                                      usingBlock:^(NSNotification *note) {
                                                          [weakSelf minimizeMyFrame];
                                                      }];
    }
    return self;
}
-(void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

-(void)resizeWithKeyboardRect:(NSValue*)newKeyboardRectFromNotification duration:(NSTimeInterval)duration
{
    LOG(@"■resizeWithKeyboardRect: => %@", newKeyboardRectFromNotification );
    
    if (!_isKeyboardShown) {
        //キーボードが表示されていない場合なにもしない
        return;
    }
    
    CGSize keyboardSize = ({
        CGSize result = CGSizeZero;
        if (newKeyboardRectFromNotification) {
            //存在する時のみ更新
            CGSize wholeKeyboardSize = [newKeyboardRectFromNotification CGRectValue].size;
            result = CGSizeMake(wholeKeyboardSize.width, [newKeyboardRectFromNotification CGRectValue].size.height - self.frame.size.height);
        }
        result;
    });
    
    if (!CGSizeEqualToSize(keyboardSize, CGSizeZero)) {
        if (_keyboardSize.height != keyboardSize.height) {
            LOG(@"resizeWithKeyboardRect:size changed from notification!: %@", NSStringFromCGSize(keyboardSize) );
            _keyboardSize = keyboardSize;
        }
    }
    
    //最終的な目標値を算出
    CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;
    CGFloat aimingHeight = MIN(
                       screenHeight - _keyboardSize.height - self.minimumUpperAreaHeight,
                       self.targetTextViewHeight
                       );
    CGRect newFrame = CGRectMake(0, 0, _keyboardSize.width, aimingHeight);
    
    [self setFrame:CGRectMake(0, aimingHeight, _keyboardSize.width, 0)];
    LOG(@"resizeWithKeyboardRect: set frame %@ => %@", NSStringFromCGRect(self.frame), NSStringFromCGRect(newFrame));
    __weak typeof(self) weakSelf = self;
    [UIView animateKeyframesWithDuration:duration delay:0 options:UIViewKeyframeAnimationOptionBeginFromCurrentState animations:^{
        [weakSelf setFrame:newFrame];
    } completion:nil];
}

-(void)minimizeMyFrame
{
    [self setFrame:CGRectMake(0, 0, _keyboardSize.width, 0)];
}
@end
```




--
おまけ:

## 日本語キーボードでの予測変換エリア出現などでキーボードのframeが変更されたり、コード側でiavのframeの変更を行ったりしたときの流れ

0. 【通知】UIKeyboardWillChangeFrameNotification
0. 【通知】UIKeyboardWillShowNotification
0. 【通知】UIKeyboardDidChangeFrameNotification
0. 【通知】UIKeyboardDidShowNotification



```objc
2014-05-22 19:32:32.408 iOS[10476:60b] NSConcreteNotification 0x170441050 {name = UIKeyboardWillChangeFrameNotification; userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 7;
    UIKeyboardAnimationDurationUserInfoKey = 0;
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 478}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 442}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 352}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 316}, {320, 252}}";
}}
2014-05-22 19:32:32.409 iOS[10476:60b] NSConcreteNotification 0x170441050 {name = UIKeyboardWillShowNotification; userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 7;
    UIKeyboardAnimationDurationUserInfoKey = 0;
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 478}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 442}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 352}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 316}, {320, 252}}";
}}
2014-05-22 19:32:32.417 iOS[10476:60b] NSConcreteNotification 0x17825a0d0 {name = UIKeyboardDidChangeFrameNotification; userInfo = {
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 478}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 442}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 352}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 316}, {320, 252}}";
}}
2014-05-22 19:32:32.418 iOS[10476:60b] : duration(0.000000), NSRect: {{0, 316}, {320, 252}}
2014-05-22 19:32:32.418 iOS[10476:60b] NSConcreteNotification 0x17825a0d0 {name = UIKeyboardDidShowNotification; userInfo = {
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 478}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 442}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 352}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 316}, {320, 252}}";
}}
```