Skip to content

UITextView をキーボードと連携させる – もともとの contentInset.bottom が 0 ではない場合

2011.04.02

“UITextView:キーボード絡みの挙動・問題点”では、いろいろなタイミングで UITextView の contentInset.bottom が強制的に変わってしまうことを説明しました。
“UITextView をキーボードと連携させる”では、UITextView のもともとの contentInset.bottom が 0 であれば、UITextView をキーボードと連携させることができることを説明しました。

今回は、UITextView のもともとの contentInset.bottom が 0 ではない場合、UITextView をどうやってキーボードと連携させるのかを紹介します。
UIEdgeInsets に関連する Tips 第5段です。

 


RLRTextView を作る

まず、“UITextView:キーボード絡みの挙動・問題点”を解決するためのクラス RLRTextView を作ります。RLRTextView は、UITextView のサブクラスで、additiveContentInsetBottom プロパティを持ちます。

@interface RLRTextView : UITextView
{
    CGFloat _additiveContentInsetBottom;
}

// Property
@property (nonatomic, assign) CGFloat additiveContentInsetBottom;

@end

additiveContentInsetBottom は、setContentInset: されたときに、引数の contentInset の bottom に付加される CGFloat です。例えば additiveContentInsetBottom をキーボードの高さ 216 に設定しておけば、UITextView が変換候補表示スペースを設定しょうと bottom が 32 や 44 の contentInset を setContentInset: してきても、最終的な contentInset.bottom は 32 + 216 や 44 + 216‥‥変換候補表示スペースの高さ + キーボードの高さになります。

- (void)setContentInset:(UIEdgeInsets)contentInset
{
    // Make inset
    UIEdgeInsets inset;
    inset = contentInset;
    inset.bottom += _additiveContentInsetBottom;
    
    // Set contentInset
    [super setContentInset:inset];
}

additiveContentInsetBottom の初期値は以下のようにして設定しています。initWithCoder: でも additiveContentInsetBottom = self.contentInset.bottom が実行されるようになっているので、RLRTextView を Interface Builder 上で作成し contentInset.bottom を弄ったとしても、additiveContentInsetBottom の値が自動的にその値になります。

- (void)_initRLRTextView
{
    // Initilize _additiveContentInsetBottom
    _additiveContentInsetBottom = self.contentInset.bottom;
}

- (id)initWithFrame:(CGRect)frame
{
    // super
    self = [super initWithFrame:frame];
    if (!self) {
        return nil;
    }
    
    // Initialize self
    [self _initRLRTextView];
    
    return self;
}

- (id)initWithCoder:(NSCoder*)decoder
{
    // super
    self = [super initWithCoder:decoder];
    if (!self) {
        return nil;
    }
    
    // Initialize self
    [self _initRLRTextView];
    
    return self;
}

RLRTextView を使う

今回は Interface Builder 上で RLRTextView を配置し、contentInset 及び scrollIndicatorInsets の top を、半透明なステータスバーの高さ + 半透明なナビゲーションバーの高さ 64 に、bottom は半透明なツールバーの高さ 44 にしました。

ビューコントローラの実装は、基本的に“UITextView をキーボードと連携させる”と同じですが、keyboardWillShow: と keyboardWillHide: の実装を以下のようにしました。

keyboardWillShow: の実装

RLRTextView とキーボードの被る高さを、additiveContentInsetBottom に設定しているところがキーです。その後 RLRTextView はいろいろなタイミングで contentInset.bottom を 32 や 44 に設定しようとしますが、additiveContentInsetBottom が付加されるので、テキストがキーボードに隠れることはありません。

- (void)keyboardWillShow:(NSNotification*)notification
{
    // Get userInfo
    NSDictionary *userInfo;
    userInfo = [notification userInfo];
    
    // Calc insetBottom
    CGFloat insetBottom;
    CGRect keyboardFrame;
    keyboardFrame = [[userInfo
        objectForKey:UIKeyboardFrameEndUserInfoKey]
        CGRectValue];
    keyboardFrame = [_textView.superview
        convertRect:keyboardFrame
        fromView:nil];
    insetBottom = CGRectGetMaxY(_textView.frame)
        - CGRectGetMinY(keyboardFrame);
    
    // Set additiveContentInsetBottom
    _textView.additiveContentInsetBottom = insetBottom;
    
    // Calc insets
    UIEdgeInsets contentInset;
    UIEdgeInsets scrollIndicatorInsets;
    contentInset = _textView.contentInset;
    scrollIndicatorInsets = _textView.scrollIndicatorInsets;
    scrollIndicatorInsets.bottom = insetBottom;
    
    // Animate insets of _textView
    NSTimeInterval duration;
    UIViewAnimationCurve animationCurve;
    void (^animations)(void);
    duration = [[userInfo
        objectForKey:UIKeyboardAnimationDurationUserInfoKey]
        doubleValue];
    animationCurve = [[userInfo
        objectForKey:UIKeyboardAnimationCurveUserInfoKey]
        integerValue];
    animations = ^(void) {
        _textView.contentInset = contentInset;
        _textView.scrollIndicatorInsets = scrollIndicatorInsets;
    };
    [UIView
        animateWithDuration:duration
        delay:0.0 options:(animationCurve << 16)
        animations:animations
        completion:nil];
    
    // Show done item
    UIBarButtonItem *doneItem;
    doneItem = [[[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemDone
        target:self
        action:@selector(doneAction:)]
        autorelease];
    [self.navigationItem
        setRightBarButtonItem:doneItem animated:YES];
}

keyboardWillHide: の実装

RLRTextView とツールバーの被る高さを additiveContentInsetBottom として設定しているところがキーです。その後の _textView.contentInset に設定している contentInset の bottom は、「変換候補表示スペースの高さは 0 にリセットする。ツールバーと被る高さは additiveContentInsetBottom の方で設定しているので加えない」という考えで 0 にしています。

- (void)keybaordWillHide:(NSNotification*)notification
{
    // Get userInfo
    NSDictionary *userInfo;
    userInfo = [notification userInfo];
    
    // Calc insetBottom
    CGFloat insetBottom;
    CGRect toolbarFrame;
    UIToolbar *toolbar;
    toolbar = self.navigationController.toolbar;
    toolbarFrame = [_textView.superview
        convertRect:toolbar.frame
        fromView:toolbar.superview];
    insetBottom = CGRectGetMaxY(_textView.frame)
        - CGRectGetMinY(toolbarFrame);
    
    // Reset additiveContentInsetBottom
    _textView.additiveContentInsetBottom = insetBottom;
    
    // Calc insets
    UIEdgeInsets contentInset;
    UIEdgeInsets scrollIndicatorInsets;
    contentInset = _textView.contentInset;
    contentInset.bottom = 0.0;
    scrollIndicatorInsets = _textView.scrollIndicatorInsets;
    scrollIndicatorInsets.bottom = insetBottom;
    
    // Animate insets
    NSTimeInterval duration;
    UIViewAnimationCurve animationCurve;
    void (^animations)(void);
    duration = [[userInfo
        objectForKey:UIKeyboardAnimationDurationUserInfoKey]
        doubleValue];
    animationCurve = [[userInfo
        objectForKey:UIKeyboardAnimationCurveUserInfoKey]
        integerValue];
    animations = ^(void) {
        _textView.contentInset = contentInset;
        _textView.scrollIndicatorInsets = scrollIndicatorInsets;
    };
    [UIView
        animateWithDuration:duration
        delay:0.0
        options:(animationCurve << 16)
        animations:animations
        completion:nil];
    
    // Hide done item
    [self.navigationItem setRightBarButtonItem:nil animated:YES];
}

おわりに

これで、もともとの contentInset.bottom が 0 ではない場合でも、UITextView をキーボードと連携させることができました。少し使い辛いクラスになってしまいました。もっとよい方法がないか、調べてみたいものです。

今回紹介した方法ですが、もし Landscape にも対応するならば、Landscape にするとナビゲーションバーやツールバーの高さが変わりますから、そのタイミングで contentInset や scrollIndicatorInsets, additiveContentInsetBottom を調節することになります。

Advertisements

From → Develop

Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s