14
9

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で宣言的に定義する例(Swift4 対応 + α)

Last updated at Posted at 2018-02-12

UITextFieldがキーボードに隠れるのを防ぐ処理をRxSwiftで宣言的に定義する例
https://qiita.com/gamako/items/160460ddb8b39e394ebe

 上記記事のSwift4対応版です。 Swift2 -> 3,4でかなり色々変わっていたので修正してみました。

 更に、引用元のコードだとTextFieldが置かれているScrollViewの上部に他のView Partsが置かれている場合にスクロール位置がずれる問題がありました。
 absPointで画面中の絶対座標を計算して位置を調整するように改善しています。

import Foundation
import UIKit
import RxSwift
import RxCocoa

extension UIViewController {
    
    // キーボードが現れたときに、テキストフィールドをスクロールする
    // https://qiita.com/gamako/items/160460ddb8b39e394ebe
    func bindScrollTextFieldWhenShowKeyboard() {
        
        var disposeBag: DisposeBag? = DisposeBag()
        
        // この関数内で完結するように、dealloc時にdisposeしてくれる仕組みを用意する
        rx.deallocating
            .subscribe(onNext: { _ in
                disposeBag = nil
            })
            .disposed(by: disposeBag!)
        
        // viewAppearの間だけUIKeyboardが発行するNotificationを受け取るObserbaleを作る
        viewAppearedObservable()
            .flatMapLatest { event -> Observable<(Bool, Notification)> in
                if event {
                    // notificationは、(true=表示/false=非表示, NSNotification)のタプルで次のObservableに渡す
                    return Observable.of(
                        NotificationCenter.default.rx.notification(Notification.Name.UIKeyboardWillShow) // Swift4.2〜 UIResponder.keyboardWillShowNotification
                            .map { (true, $0)},
                        NotificationCenter.default.rx.notification(Notification.Name.UIKeyboardWillHide) // Swift4.2〜 UIResponder.keyboardWillHideNotification
                            .map { (false, $0)}
                        ).merge()
                } else {
                    return Observable<(Bool, Notification)>.empty()
                }
            }
            .subscribe (onNext: { [weak self] (isShow: Bool, notification: Notification) in
                // notificationに対して、適切にスクロールする処理
                guard let `self` = self else { return }
                if isShow {
                    self.keyboardWillBeShown(notification: notification)
                } else {
                    self.restoreScrollViewSize(notification: notification)
                }
            })
            .disposed(by: disposeBag!)
    }
    
    /// キーボード表示時にTextFieldの位置を変更
    private func keyboardWillBeShown(notification: Notification) {
        
        guard let textField = self.view.currentFirstResponder() as? UIView,
            let scrollView = textField.findSuperView(ofType: UIScrollView.self),
            let userInfo = notification.userInfo,
            let keyboardFrame = userInfo[UIKeyboardFrameEndUserInfoKey] as? CGRect,
            let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber
            else { return }
        
        scrollView.contentInset = UIEdgeInsets.zero
        scrollView.scrollIndicatorInsets = UIEdgeInsets.zero
        
        // textFieldの画面上の絶対座標
        let textFieldAbsPoint = textField.absPoint
        
        // 画面サイズ
        let screenSize = UIScreen.main.bounds.size
        
        // textFieldの底の位置の座標
        let textPosition = textFieldAbsPoint.y + textField.frame.height
        
        // キーボード位置
        let keyboardPosition = screenSize.height - keyboardFrame.size.height
        
        // 移動判定
        if textPosition >= keyboardPosition {
            
            // 移動距離
            let offsetY = textPosition - keyboardPosition
            
            // 移動
            UIView.animate(withDuration: TimeInterval(truncating: animationDuration)) {
                let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: offsetY, right: 0) // いらないかも
                scrollView.contentInset = contentInsets // いらないかも
                scrollView.scrollIndicatorInsets = contentInsets // いらないかも
                scrollView.contentOffset = CGPoint(x: 0, y: offsetY)
            }
        }
    }
    
    /// TextFieldを元の位置に戻す
    private func restoreScrollViewSize(notification: Notification) {
        guard let textField = self.view.currentFirstResponder() as? UIView,
            let scrollView = textField.findSuperView(ofType: UIScrollView.self) else { return }
        
        scrollView.contentInset = UIEdgeInsets.zero
        scrollView.scrollIndicatorInsets = UIEdgeInsets.zero
    }
}

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
    }
    
    // 任意の型の親ビューを探す
    // 親をたどってScrollViewを探す場合などに使用する
    func findSuperView<T>(ofType: T.Type) -> T? {
        if let superView = self.superview {
            switch superView {
            case let superView as T:
                return superView
            default:
                return superView.findSuperView(ofType: ofType)
            }
        }
        
        return nil
    }
    
    /// 画面中の絶対座標
    var absPoint: CGPoint {
        var point = CGPoint(x: self.frame.origin.x, y: self.frame.origin.y)
        
        if let superview = self.superview {
            let addPoint = superview.absPoint
            point = CGPoint(x: point.x + addPoint.x, y: point.y + addPoint.y)
        }
        
        return point
    }
}

// http://blog.sgr-ksmt.org/2016/04/23/viewcontroller_trigger/
extension UIViewController {
    
    /// trigger event
    private func trigger(selector: Selector) -> Observable<Void> {
        return rx.sentMessage(selector).map { _ in () }.share(replay: 1)
    }
    
    var viewDidAppearTrigger: Observable<Void> {
        return trigger(selector: #selector(self.viewDidAppear(_:)))
    }
    
    var viewDidDisappearTrigger: Observable<Void> {
        return trigger(selector: #selector(self.viewDidDisappear(_:)))
    }
    
    func viewAppearedObservable() -> Observable<Bool> {
        return Observable.of(
            viewDidAppearTrigger.map { true } ,
            viewDidDisappearTrigger.map { false }
            )
            .merge()
    }
}
14
9
6

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
14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?