37
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-06-18

--

inputAccessoryViewとは?

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

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

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

キーボード出現の流れ

  1. UITextView, UITextFieldなどがfirstResponderとなり、ソフトウェアキーボードを要求される
  2. kbFrame、iavFrameDefaultをもとに、containerFrame(位置,サイズ)を決定
  3. 【通知】UIKeyboardWillChangeFrameNotification
  4. 【通知】UIKeyboardWillShowNotification
  5. containerFrameまでアニメーション (UIKeyboardAnimationDurationUserInfoKey > 0)
  6. 【通知】UIKeyboardDidChangeFrameNotification
  7. 【通知】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を下からニュッと出すアニメーションをさせる

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

@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の変更を行ったりしたときの流れ

  1. 【通知】UIKeyboardWillChangeFrameNotification
  2. 【通知】UIKeyboardWillShowNotification
  3. 【通知】UIKeyboardDidChangeFrameNotification
  4. 【通知】UIKeyboardDidShowNotification
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}}";
}}
37
41
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?