This post explains how to adjust content's position when a keyboard shows and dismisses. It is utilized by RxSwift. The key is creating an associated with UIResponder. This post also explains how to dismiss a keyboard when tapping an enter-key or when tapping outside of the keyboard with RxSwift.
There are a lot of articles implementing these features by using just Notification center and @objc
functions. However, there is little info about applying RxSwift. Besides, if we use RxSwift, we can make it more simple, concise. No more @objc
nor delegate
By default, there is no method to dismiss a keyboard when you input "Enter" or when you tap outside of the keyboard. If you want to implement, you need delegate
or # selector
.
When you put a textField and if its position is bottom, it will be hidden behind the keyboard so users can't see any results!
All problems can be solved by using RxSwift (and RxCocoa)
In short, here is the code.
// Instance variables
let textField = UITextField()
let disposeBag = DisposeBag()
// When enter, dismiss keyboard
textField.rx
.controlEvent(.editingDidEndOnExit)
.bind {}
.disposed(by: disposeBag)
Instead of using delegate function textFieldDidEndEditing(_:)
, we can just use rx
(reactive extension) of UITExtField, and UIControl.Event
. In the bind closure, you can put whatever completion invoked after the keyboard is dismissed.
In short, here is the code
// Add Gesture: when tap out of keyboard, dismiss it
let tapGesture = UITapGestureRecognizer()
view.addGestureRecognizer(tapGesture)
tapGesture.rx.event
.bind { [weak self] _ in
self?.view.endEditing(true)
}
.disposed(by: disposeBag)
We create a gesture recognizer and embed it into the view. This is the same as usual. However, instead of adding target, we use rx.event
, which will emit an event (UIGestureRecognizer
) when it is recognized. Actually, we don't use the event but use it as a trigger to call view.endEditing(true)
this time.
The great point of these ways is not only simplicity. Because we can avoid using #selector
and @objc
functions, we can all write these codes in the same place.
The conclusion is we can apply like this.
// Emit height when keyboard shows
let willShownObservable = NotificationCenter.default
.rx.notification(UIResponder.keyboardWillShowNotification)
.compactMap({ notification -> CGRect? in
notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
})
.map { rect in
return rect.height
}
// Emit 0 when keyboard dismiss
let willHideObservable = NotificationCenter.default
.rx.notification(UIResponder.keyboardWillHideNotification)
.map { _ -> CGFloat in
return 0
}
// combine observables and bind to transform
Observable.of(willShownObservable, willHideObservable)
.merge()
.bind { [weak view = self.view] height in
view?.transform = CGAffineTransform(translationX: 0, y: -height)
}
.disposed(by: disposeBag)
What we are doing is we are creating two Observable
(Publisher), one emits the keyboard height when it displays, the other emits 0 when it dismisses. After that, we merge
two "observables" and bind them to modify the height of the view.
Through rx extension, we can also make the NotificationCenter
emit event(notification).
The Observable of NotificationCenter is UIResponder
. Thus,
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
means like a "NotificationCenter is looking a keyboard's behavior as Observer
and also emit notification as Observable
when the keyboard updates".
keyboard <---- NotificationCenter ----> compact, map ---> height
After that, we're just operating (cleaning) the event. We extract keyboard height from notification using compactMap
and map
. compactMap
is just a filter no-nil
object.
The final essence is merging two Observable
and subscribe to them.
Observable.of(willShownObservable, willHideObservable)
.merge()
This code merges two observables and creates another Observable
. This means, whatever subscribes to this observable will get events from both keyboard displays and dismiss. The actions are the trigger. By using the merge()
, we can avoid subscribing to each Observable.
view?.transform = CGAffineTransform(translationX: 0, y: -height)
This is just changing the bounds
(center) by transform
. You can change NSLayoutConstraint
or change a contentInset
of the scroll view. It depends on the situation and is up to you!
RxSwift is so powerful. The codes that I introduce this time can be implemented in one section. We can apply a reactive framework to a lot of existing features such as delegate, target(#selector), completion handler, and so on.
I put the sample project in my repository so you can check the code easily!