17
16

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.

UITextFieldがキーボードに隠れるのを防ぐ処理をRxSwiftで宣言的に定義する例

Last updated at Posted at 2016-05-12

Swift4対応+改良された記事

こちらの記事をご参考にどうぞ

以下古い内容

RxSwift/RxCocoaで書くと、notificationの処理や、method-swizzlingを使ってviewDidAppearまわりもトリガー化できます。

viewが現れた後だけNotificationを受けとって何かするというような処理は、本来は幾つかのメソッドをUIViewControllerごとに書かなければならなかったりしてなかなか面倒臭い定型処理ですが、RxSwift/RxCocoaなら宣言的に書くことができます。
今回は1メソッド呼ぶだけでよいように収めることができました。

コードはこちらにも置きました。
https://gist.github.com/gamako/6f7df33badaf279cc313cb934728a792

コード


import UIKit
import RxSwift
import RxCocoa

extension UIViewController {
    
    // キーボードが現れたときに、テキストフィールドをスクロールする
    func scrollWhenShowKeyboard() {
        
        var disposeBag : DisposeBag? = DisposeBag()
        
        // この関数内で完結するように、dealloc時にdisposeしてくれる仕組みを用意する
        rx_deallocating.subscribeNext { disposeBag = nil }.addDisposableTo(disposeBag!)
        
        // viewAppearの間だけUIKeyboardが発行するNotificationを受け取るObserbaleを作る
        viewAppearedObservable().flatMapLatest
            { (b) -> Observable<(Bool, NSNotification)> in
                if b {
                    // notificationは、(true=表示/false=非表示, NSNotification)のタプルで次のObservableに渡す
                    return Observable.of(
                        NSNotificationCenter.defaultCenter().rx_notification(UIKeyboardWillShowNotification).map { (true, $0)},
                        NSNotificationCenter.defaultCenter().rx_notification(UIKeyboardWillHideNotification).map { (false, $0)}
                        ).merge()
                } else {
                    return Observable<(Bool, NSNotification)>.empty()
                }
            }
            .subscribeNext { [weak self] (a: (isShow:Bool, notification:NSNotification)) in
                // notificationに対して、適切にスクロールする処理
                if a.isShow {
                    self?.scrollTextFieldWhehKeybordShown(a.notification)
                } else {
                    self?.restoreScrollTextField(a.notification)
                }
            }.addDisposableTo(disposeBag!)
    }
    
    // キーボードのframeとUITextFieldの位置を比較して、いい感じにスクロールする処理
    // 表示するターゲットは現在のFirstResponder
    // スクロールするUIScrollViewは、その親をたどって見つけている
    //
    // 参考 : http://qiita.com/ysk_1031/items/3adb1c1bf5678e7e6f98
    private func scrollTextFieldWhehKeybordShown(notification: NSNotification) {
        guard let textField = self.view.currentFirstResponder() as? UIView,
            scrollView = textField.findSuperView(UIScrollView.self),
            userInfo = notification.userInfo,
            keyboardFrame = userInfo[UIKeyboardFrameEndUserInfoKey]?.CGRectValue,
            animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey]?.doubleValue
            else { return }
        
        scrollView.contentInset = UIEdgeInsetsZero
        scrollView.scrollIndicatorInsets = UIEdgeInsetsZero
        
        let convertedKeyboardFrame = scrollView.convertRect(keyboardFrame, fromView: nil)
        let convertedTextFieldFrame = textField.convertRect(textField.frame, toView: scrollView)
        
        let offsetY = CGRectGetMaxY(convertedTextFieldFrame) - CGRectGetMinY(convertedKeyboardFrame)
        if offsetY > 0 {
            
            UIView.animateWithDuration(animationDuration) {
                let contentInsets = UIEdgeInsetsMake(0, 0, offsetY, 0)
                scrollView.contentInset = contentInsets
                scrollView.scrollIndicatorInsets = contentInsets
                scrollView.contentOffset = CGPointMake(0, offsetY)
            }
        }
    }
    
    // スクロールを元に戻す
    private func restoreScrollTextField(notification: NSNotification) {
        guard let textField = self.view.currentFirstResponder() as? UIView,
            let scrollView = textField.findSuperView(UIScrollView.self)
            else { return }
        
        scrollView.contentInset = UIEdgeInsetsZero
        scrollView.scrollIndicatorInsets = UIEdgeInsetsZero
    }
    
}

extension UIView {
    
    // 親ビューをたどってFirstResponderを探す
    func currentFirstResponder() -> UIResponder? {
        if self.isFirstResponder() {
            return self
        }
        
        for view in self.subviews {
            if let responder = view.currentFirstResponder() {
                return responder
            }
        }
        
        return nil
    }
    
    // 現在入力中のUITextFieldやUITextViewのカーソル位置に文字を挿入する
    func insertTextToCurrentField(text : String) {
        guard let field = self.currentFirstResponder() as? UIKeyInput else { return }
        field.insertText(text)
    }
    
    // 任意の型の親ビューを探す
    // 親をたどってScrollViewを探す場合などに使用する
    func findSuperView<T>(ofType: T.Type) -> T? {
        if let s = self.superview {
            switch s {
            case let s as T:
                return s
            default:
                return s.findSuperView(ofType)
            }
        }
        return nil
    }
    
}

extension UIViewController {
    
    // ViewがdidAppearでtrue, didDisappearでfalseになるobservable
    func viewAppearedObservable() -> Observable<Bool> {
        return Observable.of(
            viewDidAppearTrigger.map { true } ,
            viewDidDisappearTrigger.map { false }
            )
            .merge()
    }
}

// http://blog.sgr-ksmt.org/2016/04/23/viewcontroller_trigger/
extension UIViewController {
    private func trigger(selector: Selector) -> Observable<Void> {
        return rx_sentMessage(selector).map { _ in () }.shareReplay(1)
    }
    
    var viewWillAppearTrigger: Observable<Void> {
        return trigger(#selector(self.viewWillAppear(_:)))
    }
    
    var viewDidAppearTrigger: Observable<Void> {
        return trigger(#selector(self.viewDidAppear(_:)))
    }
    
    var viewWillDisappearTrigger: Observable<Void> {
        return trigger(#selector(self.viewWillDisappear(_:)))
    }
    
    var viewDidDisappearTrigger: Observable<Void> {
        return trigger(#selector(self.viewDidDisappear(_:)))
    }
}

備考

Rx以外の面で工夫した点としては、

  • 現在エディット中のフィールドは、すべての子ViewからFirstResponderを探すことで取得する
  • スクロールすべきUIScrollViewは、エディット中のフィールドの親をたどってUIScrollViewを探す
  • これらにより、Viewの構成によらずにスクロール処理を行うことができました

参考サイト

http://qiita.com/ysk_1031/items/3adb1c1bf5678e7e6f98
http://blog.sgr-ksmt.org/2016/04/23/viewcontroller_trigger/

17
16
1

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
17
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?