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

# Swift4対応＋改良された記事

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

https://qiita.com/takkei/items/b5cb23d36667596e86ce

# 以下古い内容

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

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


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


# コード

```swift

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/