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/