I have a view controller in which it has
table view and textView below the table
When I tap on the textView the keyboard appears and when I tap outside the textView the keyboard disappar
That works fine but I have one problem which is when I Drag(scroll Down) the table view the textView doesn't move down with the keyboard and I see black background behind the keyboard as in the below image
How to solve this problem in Swift
Update:
This is my current code that observe the keyboardFrameChanges
func keyboardFrameChanged(notification: NSNotification) {
let dict = NSDictionary(dictionary: notification.userInfo!)
let keyboardValue = dict.objectForKey(UIKeyboardFrameEndUserInfoKey) as! NSValue
let bottomDistance = mainScreenSize().height - keyboardValue.CGRectValue().origin.y
let duration = Double(dict.objectForKey(UIKeyboardAnimationDurationUserInfoKey) as! NSNumber)
UIView.animateWithDuration(duration, animations: {
self.inputViewConstraint!.constant = -bottomDistance
self.view.layoutIfNeeded()
}, completion: {
(value: Bool) in
self.chatTableView.scrollToBottom(animation: true)
})
}
Update 2:
I found a solution by changing the keyboardDismissMode from Interactive to OnDrag
chatTableView.keyboardDismissMode = UIScrollViewKeyboardDismissMode.OnDrag
This will move the keyboard and the textView immediatly to down once it observe any drag movement in the table , But how to do it in the Interactive mode like in the whatsapp chat view
set you scroll view (UITableView or UICollectionView) frame equal to you UIViewController frame
set .interactive to keyboardDismissMode of you scroll view
override canBecomeFirstResponder and return true
setup input container with UITextField or UITextView in screen bottom
override inputAccessoryView and return input container
enjoy
See my simple code here on github
If you implement your own bottom constraint for input view
And change its constant on the keyboard frame notifications. Using these lines of code, the error of predictive bar layout at the end of the animation will be fixed.
override var inputAccessoryView: UIView? {
return UIView()
}
override var canBecomeFirstResponder: Bool {
return true
}
Finding the answer for this took forever but this is the correct way of implementing this feature with the interactive state enabled.
In your view controller that has the tableview or collection view displaying chat bubbles override inputAccessoryView, then return the view that contains the uitextView or uitextField. and override canBecomeFirstResponder then return true.
override var inputAccessoryView: UIView? {
get {
self.inputBar.frame.size.height = self.barHeight // 50.0
self.inputBar.clipsToBounds = true
return self.inputBar
}
}
override var canBecomeFirstResponder: Bool{
return true
}
You could observe the frame of the keyboard using the NSNotificationCenter and create a method which will be called when the frame changes. In this method you get the coordinates of the keyboard and can reposition your inputView accordingly.
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.keyboardDidChangeFrame(_:)), name: UIKeyboardWillChangeFrameNotification, object: nil)
}
func keyboardDidChangeFrame(notification: NSNotification) {
let keyboardScreenFrameEnd = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
let keyboardViewFrameEnd = view.convertRect(keyboardScreenEndFrame, fromView: view.window)
let keyboardHeight = keyboardViewFrameEnd.height
// calculate the offset of your inputView
inputView.frame = CGRectOffset(inputView?.frame, ..., ...)
}
Related
I'm trying to implement an input accessory view that works just like Messages app in iOS. I've searched almost every SO questions regarding this topic, but couldn't find the solution that worked for me.
Here is the minimal reproducible code I created, referring to this SO post.
import UIKit
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
becomeFirstResponder() // seems unnecessary
}
override var inputAccessoryView: UIToolbar {
return self.keyboardAccessory
}
override var canBecomeFirstResponder: Bool {
return true
}
var textView: UITextView = {
let view = UITextView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
lazy var keyboardAccessory: UIToolbar = {
let inputAccessory = UIToolbar(frame: .init(x: 0, y: 0, width: 0, height: 100))
inputAccessory.addSubview(textView)
NSLayoutConstraint.activate([
textView.centerXAnchor.constraint(equalTo: inputAccessory.centerXAnchor),
textView.centerYAnchor.constraint(equalTo: inputAccessory.centerYAnchor),
textView.widthAnchor.constraint(equalToConstant: 200),
textView.heightAnchor.constraint(equalToConstant: 50)
])
inputAccessory.backgroundColor = .gray
return inputAccessory
}()
}
Every article I've seen suggests overriding inputAccessoryView and canBecomeFirstResponder, and that's it. However, the keyboard does not appear until I tap the textView.
Can anyone let me know what I'm missing?
Edit
As #DonMag pointed out, Messages app in iOS does not show keyboard automatically. Please consider following UI in Facebook instead.
When I press the comment button, it pushes to another view controller while popping up the keyboard. The transition effect doesn't have to be exactly the same, but I want the keyboard become fully loaded within presented view controller, as if I called becomeFirstResponder() in viewDidLoad.
If you want the text view to become active, and the keyboard to show, as soon as the view appears, use:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textView.becomeFirstResponder()
}
If you want the text view to be visible at the bottom, and become active / show the keyboard when the textview is tapped, take a look at this answer:
https://stackoverflow.com/a/61508928/6257435
Edit
If you want to push a view controller onto the navigation stack, and have the keyboard showing with a custom input accessory view, containing a text view, and give it the focus...
Add a hidden text field to the controller. In viewDidLoad tell that text field to use the custom input accessory view and tell it to become first responder.
Then, in viewDidAppear tell the text view in the custom input accessory view to become the first responder:
class TestViewController: UIViewController {
var hiddenTF = UITextField()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// set the text field to hidden
hiddenTF.isHidden = true
// add it to the view
view.addSubview(hiddenTF)
// tell hidden text field to use custom input accessory view
hiddenTF.inputAccessoryView = keyboardAccessory
// tell it to become first responder
hiddenTF.becomeFirstResponder()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// tell the textView (in the custom input accessory view)
// to become first responder
textView.becomeFirstResponder()
}
var textView: UITextView = {
let view = UITextView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
lazy var keyboardAccessory: UIToolbar = {
let inputAccessory = UIToolbar(frame: .init(x: 0, y: 0, width: 0, height: 100))
inputAccessory.addSubview(textView)
NSLayoutConstraint.activate([
textView.centerXAnchor.constraint(equalTo: inputAccessory.centerXAnchor),
textView.centerYAnchor.constraint(equalTo: inputAccessory.centerYAnchor),
textView.widthAnchor.constraint(equalToConstant: 200),
textView.heightAnchor.constraint(equalToConstant: 50)
])
inputAccessory.backgroundColor = .gray
return inputAccessory
}()
}
Actually when you override canBecomeFirstResponder the keyboard is appear just under the view , thats why you only see the accessory view bottom side of the view . You can basically try this with adding notification to your controller like
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name:UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func keyboardWillShow(notification:NSNotification) {
let userInfo = notification.userInfo!
let keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
print(keyboardFrame)
print(self.view.frame)
}
When you run the project , you gonna see the keyboardWillShow notification is hired.(If you delete overiride canBecomeFirstResponder it won't )
And when you print keyboard and view frame , you gonna notice to keyboard's y position is equal to view's frame height . That means keyboards want to show us only its accessoryView .
So , you need to hired textView.becomeFirstResponder() in keyboardWillShow notification
#objc func keyboardWillShow(notification:NSNotification) {
let userInfo = notification.userInfo!
let keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
textView.becomeFirstResponder()
}
Do not forget to deinit notification when controller deinit
deinit {
NotificationCenter.default.removeObserver(self)
}
I have a UITextView at the top, a UITextView in the centre and a UITextView at the bottom.
I want to move the view up when the keyboard presents if using the bottom UITextView or the centre UITextView but when using the top UITextView the view shouldn't move.
How do I make this work?
func showLoginKeyBoard()
{
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification)
{
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
{
if self.view.frame.origin.y == 0
{
self.view.frame.origin.y -= keyboardSize.height
}
}
}
func textViewDidBeginEditing(_ textView: UITextView)
{
if textView == centreTextView
{
showLoginKeyBoard()
}
if textView == bottomTextView
{
showLoginKeyBoard()
}
}
Currently when any of the UITextViews becomeFirstResponder the view moves up which means when using the top UITextView it isn't visible.
How can I make sure the top UITextView doesn't move the view up?
Before answer your question ,
According to your code every time user click on textView you add Observer.Don't do this. Add observers at viewDidLoad() and don’t forget to remove observers in viewDidDisappear(). Otherwise it will cause to memory leaks.
Now,Answer to question
define fileprivate optional textView
var currentTextView:UITextView?
Then assign textField in textViewDidBeginEditing
func textViewDidBeginEditing(_ textView: UITextView){
currentTextView = textView
}
now you can show or not according to currentTextView
#objc func keyboardWillShow(notification: NSNotification){
if let txtView = currentTextView{
txtView != topTextView {
//move up the view
}
}
}
I recommend using IQKeyboardManager library which handles all your keyboard events with only a single line of code
I would like to customize the scroll-offset when showing the keyboard. As you can see in the GIF, the Textfields are quite close to the keyboard and I would like to have a custom position. The "Name" textfield should have 50px more distance and the "Loan Title" textfield should just scroll to the bottom of my UIScrollView.
To be able to scroll past the keyboard I'm changing the UIScrollView insets. Strangely iOS automatically scrolls to the firstResponder textfield (see GIF).
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
// get the Keyboard size
let userInfo = notification.userInfo!
let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// update edge insets for scrollview
self.mainScrollView.scrollIndicatorInsets.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
self.mainScrollView.contentInset.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
}
I already tried to use the UITextfieldDelegate method: textFieldDidBeginEditing(_ textField: UITextField)
I also tried to use the Apple way described here: https://stackoverflow.com/a/28813720/7421005
None of these ways let me customize the automatic scroll position. In fact it kind of overrides every attempt. Does anyone know a way to workaround this?
You can prevent your view controller from automatically scrolling by setting automaticallyAdjustsScrollviewInsets to false as described here.
Implementing keyboard avoidance is also pretty straight forward. You can see how to do it here.
I don't believe there is any way to keep the automatic positioning and apply your own custom offset. You could experiment with making text field contained in another larger view and making that larger view the first responder, but that would be a hack at best.
I found a solution by myself. The problem was that the automatic scroll (animation) was interfering with my scrollRectToVisible call. Putting this in async fixed the problem.
It now looks similar to this:
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
// get the Keyboard size
let userInfo = notification.userInfo!
let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// update edge insets for scrollview
self.mainScrollView.scrollIndicatorInsets.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
self.mainScrollView.contentInset.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
var frame = CGRect.zero
if nameTextField.isFirstResponder {
frame = CGRect(x: nameTextField.frame.origin.x, y: nameTextField.frame.origin.y + 50, width: nameTextField.frame.size.width, height: nameTextField.frame.size.height)
}
if titleTextField.isFirstResponder {
frame = CGRect(x: titleTextField.frame.origin.x, y: titleTextField.frame.origin.y + titleShortcutsCollectionView.frame.height + 25, width: titleTextField.frame.size.width, height: titleTextField.frame.size.height)
}
DispatchQueue.main.async {
self.mainScrollView.scrollRectToVisible(frame, animated: true)
}
}
I have a rather complex situation. It has to do with moving a view above the keyboard. I have a chat view, much like the one Apple's Messages app. However, it's inside of a card that does not fill the entire screen. Above and below that card are a navigation bar and a tab bar.
Now, I want to move the editable UITextView (where the user composes their message) above the keyboard when they tap on it. I do have the height of the keyboard (using NotificationCenter Observer), but I need to subtract it from the height of the textview (inside of the card) from the height of the keyboard to calculate how much distance I should offset the textview by. A picture would probably help:
Here is my view stack:
View -> Card view -> ScrollView (that contains messages list tableview and chat tableview) -> TextBox View (that contains the UITextView, Send Button, etc.)
Is this doable? Is there a better way of doing this? I'm stuck and am pretty new at manipulating views in regards to the keyboard.
Here are scraps of code I have so far:
fileprivate var keyboardHeight = CGFloat()
#objc fileprivate func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardHeight = keyboardSize.height
self.keyboardHeight = keyboardHeight
}
}
fileprivate func moveTextView(textView: UITextView, moveDistance: Int, up: Bool) {
let moveDuration = 0.3
let movement = CGFloat(up ? moveDistance : -moveDistance)
UIView.animate(withDuration: moveDuration) {
self.snp.updateConstraints({ (make) in
make.bottom.equalToSuperview().offset(movement)
})
self.superview?.layoutIfNeeded()
}
}
extension TextBarView: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
moveTextView(textView: textView, moveDistance: -200, up: true)
}
func textViewDidEndEditing(_ textView: UITextView) {
moveTextView(textView: textView, moveDistance: -200, up: false)
}
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
textView.resignFirstResponder()
return true
}
}
I am using SnapKit for autolayout.
(If you got through this entire post, thank you!)
Update
I found that in my many refactorings I was inheriting from UIViewController instead of UITableViewController, so I was missing some automatic behaviours that UITableViewController provides. However, I still needed to manually handle the scroll views insets when the keyboard was being interactively dismissed. See my updated answer.
I am trying to emulate iMessage in how the keyboard is dismissed when the user drags it to the bottom of the screen. I have it working with one small visual issue that's bugging me.
As the keyboard is dragged off the screen the scroll indicators do not resize correctly - that is until it has been completely dismissed.
I use keyboard notifications to tell me when the keyboard has appeared to increase the content and scroll insets by the height of the keyboard. It seems I didn't need to do anything when the keyboard has been dismissed as the insets appear to be correct when it has been. However when dismissing interactively I can't update the insets during the dragging event.
To illustrate the issue, the first image shows that content has scrolled off the top of the screen due to the space being occupied by the keyboard; the user has scrolled to the last row in the table:
Here, the keyboard is being dismissed and is almost completely off-screen. However notice how the scroll indicators are completely the wrong size. All of the content is now almost on screen so the indicators should be stretching, however, what happens is that as the keyboard moves down, the scroll indicators move up and do not stretch. This is not what happens in iMessage.
I think what I'm doing is pretty standard, I'm creating a UIToolBar (iOS 8.3) and overriding these methods in my view controller:
override var inputAccessoryView: UIView {
return toolbar
}
override func canBecomeFirstResponder() -> Bool {
return true
}
func willShowKeyboard(notification: NSNotification) {
let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
tableView.contentInset.bottom = keyboardFrame.CGRectValue().height
tableView.scrollIndicatorInsets.bottom = keyboardFrame.CGRectValue().height
}
Update
After switching to a UITableViewController, I found that this implementation of scrollViewDidScroll() (along with the other methods in the original solution below) did the trick of dynamically resizing the insets when the keyboard was interactively dismissed.
override func scrollViewDidScroll(scrollView: UIScrollView) {
if !keyboardShowing {
return
}
let toolbarFrame = toolbar.convertRect(toolbar.frame, toView: nil)
tableView.scrollIndicatorInsets.bottom = view.bounds.height - toolbarFrame.minY
tableView.contentInset.bottom = view.bounds.height - toolbarFrame.minY
}
I've managed to achieve the same effect. I'm not sure if this is the correct method, but it works nicely. I'll be interested to know what other solutions there might be.
func didShowKeyboard(notification: NSNotification) {
let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
let keyboardHeight = keyboardFrame.CGRectValue().height
tableView.contentInset.bottom = keyboardHeight
tableView.scrollIndicatorInsets.bottom = keyboardHeight
keyboardShowing = true
}
func didHideKeyboard(notification: NSNotification) {
keyboardShowing = false
}
func scrollViewDidScroll(scrollView: UIScrollView) {
if !keyboardShowing {
return
}
let toolbarFrame = view.convertRect(toolbar.frame, fromView: toolbar)
tableView.scrollIndicatorInsets.bottom = view.bounds.height - toolbarFrame.minY
tableView.contentInset.bottom = view.bounds.height - toolbarFrame.minY
}