--
inputAccessoryViewとは?
ソフトウェアキーボードの上につけられる、カスタマイズ可能なViewです。キーボード自体をいじれないので、オプションのボタンを設置したりするときに便利ですよね。キーボードが消えるときにも一緒に消えてくれるし。
ただ、そのビューの高さが固定なら全然ラクチンなんですが、もし可変の場合(任意数のボタンが出るとか、さらにはキーボード操作中にボタンの数が増減するとか)、とたんにめんどくさいことになります。
「画面内の上xxxピクセルはテキストビューの領域として確保しよう」って思ったとしても、そのxxxピクセルをどう求めるのか?そして、求めた値をどのタイミングでsetFrame:するのか?というのが、かなり大変なのです。
この問題について、当記事では語ろうと思います。
便宜上、以下のように呼びます。
- iav = inputAccessoryView。キーボードの上に補助的に表示できるやつ
- キーボード = 見慣れたソフトウェアキーボード
- コンテナ = iavとキーボードを含めたもの
- iavFrameDefault = iavに設定されているframe
- iavFrameActual = iavに本当に設定したいframe
- kbFrame = キーボードのframe
- containerFrame = コンテナのframe
キーボード出現の流れ
- UITextView, UITextFieldなどがfirstResponderとなり、ソフトウェアキーボードを要求される
- kbFrame、iavFrameDefaultをもとに、containerFrame(位置,サイズ)を決定
- 【通知】UIKeyboardWillChangeFrameNotification
- 【通知】UIKeyboardWillShowNotification
- containerFrameまでアニメーション (UIKeyboardAnimationDurationUserInfoKey > 0)
- 【通知】UIKeyboardDidChangeFrameNotification
- 【通知】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の変更を行ったりしたときの流れ
- 【通知】UIKeyboardWillChangeFrameNotification
- 【通知】UIKeyboardWillShowNotification
- 【通知】UIKeyboardDidChangeFrameNotification
- 【通知】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}}";
}}