I'm using UIView Extension for button to slide it up with keyboard.
extension UIView {
func bindToKeyboard() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
#objc func keyboardWillChange(_ notification: NSNotification) {
let duration = notification.userInfo![UIKeyboardAnimationDurationUserInfoKey] as! Double
let curve = notification.userInfo![UIKeyboardAnimationCurveUserInfoKey] as! UInt
let startingFrame = (notification.userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let endingFrame = (notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let deltaY = endingFrame.origin.y - startingFrame.origin.y
let options = UIViewAnimationOptions(rawValue: curve << 16)
UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
self.frame.origin.y += deltaY
self.layoutIfNeeded()
}, completion: nil)
}
}
Then in ViewController just using :
func setUpView() {
okayButton.bindToKeyboard()
self.isHeroEnabled = true
}
But the problem is when I press other button on the screen:
Save button disappears after tapping on other button, when it is in the "upper position", and appears when it's on the bottom. What am I doing wrong? How to prevent/fix it?
Edit: There is no action on any of these buttons! (+,-,save)
Thanks!
You don't necessarily need to update self.view . What you can do is create a IBOutlet bottom spacing for the save button.
#IBOutlet weak var saveButtonBottomSpacing: NSLayoutConstraint!
When keyboard is open, set bottom spacing constant to keyboard's height.
When keyboard is dismissed, restore the bottom spacing. May be 0 or your desired value.
You can make this changes within UIView animation block.
Hide (resign) your keyboard upon successful action on 'Save' button.
Here is sample code you need to update in your Save button action.
#IBAction func btnSave(sender: Any){
// add this line in your upon, successful action on save button
yourTextView.resignFirstResponder()
}
Related
I am trying to dismiss the keyboard on swipe down but my input accessory view (custom uiview class) is not sticking to it on swipe down. It leaves a space between it and the keyboard and is not syncing up with it and goes down only after the keyboard disappears. It works fine when activating the keyboard by tapping on the text view/cmd+k to toggle it up and down, but not when swiping it down. From the image I am swiping down to dismiss the keyboard but there is a giant gap. [1]: https://i.stack.imgur.com/xLV6u.jpg (Sorry if my formatting is bad, I'm still getting used to actually posting on stackoverflow)
Here is my code so far pertaining to it:
// accessory view is anchor to the bottom to start
func layoutInputAccessoryView() {
view.addSubview(inputFieldAccessoryView)
inputFieldAccessoryView.anchor(right: view.rightAnchor, left: view.leftAnchor)
inputFieldAccessoryViewBottomAnchor = inputFieldAccessoryView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)
inputFieldAccessoryViewBottomAnchor?.isActive = true
}
// swiping to dismiss in collection view
collectionView.keyboardDismissMode = .interactive
// listener for when the keyboard shows/hides
func addKBObserver() {
NotificationCenter.default.addObserver(self, selector: #selector(handleKBWilShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKBWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
// method for when the keyboard shows
#objc func handleKBWilShow(notification: Notification) {
if let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let rect = frame.cgRectValue
let height = rect.height
inputFieldContainerBottom?.constant = -height
view.layoutIfNeeded()
}
}
// selector method called when keyboard will hide
#objc func handleKBWillHide(notification: Notification) {
if let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double {
UIView.animate(withDuration: duration) {
self.inputFieldContainerBottomAnchor?.constant = 0
self.view.layoutIfNeeded()
}
}
}
I've added a toolbar above keyboard to show Done button to dismiss keyboard. I've added it on my login screen. When keyboard is showing and I tap on saved Password icon to select saved password, keyboard hides but toolbar doesn't hide. Toolbar sits at the bottom of screen and then moves up with keyboard when keyboard shows again. It looks bad.
How do I fix it so that Toolbar doesn't show on it's own and shows/hide only with keyboard?
override func viewDidLoad() {
super.viewDidLoad()
self.emailTextField.addDoneButton(title: "Done", target: self, selector: #selector(tapDone(sender:)))
self.passwordTextField.addDoneButton(title: "Done", target: self, selector: #selector(tapDone(sender:)))
}
#objc func tapDone(sender: Any) {
self.view.endEditing(true)
}
extension UITextField {
// Add done button above keyboard
func addDoneButton(title: String, target: Any, selector: Selector) {
let toolBar = UIToolbar(frame: CGRect(origin: .zero, size: CGSize(width: UIScreen.main.bounds.size.width, height: 44.0)))
let flexible = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let barButton = UIBarButtonItem(title: title, style: .plain, target: target, action: selector)
barButton.setTitleTextAttributes([NSAttributedString.Key.font : UIFont.main, NSAttributedString.Key.foregroundColor : UIColor.red], for: [])
toolBar.setItems([flexible, barButton], animated: false)
self.inputAccessoryView = toolBar
}
}
I personally handle this in a different way as I'm not using any toolbar but custom views above the keyboard. As I want those views to animate and appear/disappear depending on the keyboard position, I first listen to keyboard changes:
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
And then handle the keyboard current size and position manually here like so:
func keyboardChanged(_ userInfo: Dictionary<AnyHashable, Any>?) {
if let userInfo = userInfo {
let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let duration:TimeInterval = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue
let animationCurve:UIView.AnimationOptions = UIView.AnimationOptions(rawValue: animationCurveRaw)
if (endFrame?.origin.y)! >= UIScreen.main.bounds.size.height {
// keyboard is masked, you can mask/move your toolbar here
} else {
// update your toolbar visibility and/or position/constraints here using 'endFrame?.size.height'
}
// animate your toolbar or any other view here:
UIView.animate(withDuration: duration,
delay: TimeInterval(0),
options: animationCurve,
animations: {
// animate what you need here
self.view.layoutIfNeeded()
},
completion: nil)
}
}
So in your case I would create the toolbar first and constraint it to the bottom of the screen. Then I would use the code above to handle its position and visibility.
Then, whenever the keyboard position is updated, you can handle the toolbar (position and visibility) in the keyboard notification handler shown above.
Might not be a direct answer to this question but I highly suggest you to take a look at the IQKeyboardManager library. By default, it is a one liner keyboard handler but you can add your accessory views easily and it manages them well
https://github.com/hackiftekhar/IQKeyboardManager
I'm stuck with some funny problem and ran out of ideas how to solve it.
In one of my controllers I use a simple scheme of adjusting a view frame according to the keyboard appearance.
In UITextFieldDelegate method I initialise the controller's property firstResponder:
func textFieldDidBeginEditing(_ textField: UITextField) {
self.firstResponder = textField
}
Then I use UIKeyboard notifications selectors to change the frame of contentView:
override func keyboardWillShow(_ notifications: Notification) {
super.keyboardWillShow(notifications)
let info = notifications.userInfo
let keyboardFrame:CGRect = (info![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let duration:Double = (info![UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
var bottomY:CGFloat!
if self.firstResponder == self.emailTextField{
bottomY = self.emailBottomLine.frame.origin.y + 80 + self.headerView.frame.height
}
else {
return
}
if bottomY >= keyboardFrame.origin.y {
let offset = bottomY - keyboardFrame.origin.y
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = -offset
})
}else{
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = self.contentViewOriginY
})
}
}
override func keyboardWillHide(_ notifications: Notification) {
super.keyboardWillHide(notifications)
let info = notifications.userInfo
let duration:Double = (info![UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = self.contentViewOriginY
})
}
And everything works fine until I start typing inside the emailTextField. Each tap on the keyboard causes the contentView reset to its original position without animation.
The question is what really causes this behavior? I'm totally confused and have checked whatever thing is possibly affects this. Please, help!!!
Make sure either:
A. Your contentView does not have any layout constraints attached to it, or else when you set its frame, its frame will be reset on the next layout pass back to what the constraints say the frame should be.
or:
B. Use a constraint to position your contentView's vertical offset relative to the keyboard instead of adjusting its frame.
Sorry if any similar question(s) has been answered. But, I just can't seem to figure this one out.
I have reached my goal, to bind the "Log In" button to the keyboard, basically pushing it from the bottom of the screen to the top of the keyboard using an extension below.
picture: Initial view without keyboard.
picture: keyboard has been launched.
My UIView extension:
import UIKit
extension UIView{
func bindToKeyboard(){
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
#objc func keyboardWillChange(_ notification: NSNotification){
let duration = notification.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
let curve = notification.userInfo![UIResponder.keyboardAnimationCurveUserInfoKey] as! UInt
let beginningFrame = (notification.userInfo![UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let endFrame = (notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let deltaY = endFrame.origin.y - beginningFrame.origin.y
UIView.animateKeyframes(withDuration: duration, delay: 0.0, options: UIView.KeyframeAnimationOptions(rawValue: curve), animations: {
self.frame.origin.y += deltaY
}, completion: nil)
}
}
and I called bindToKeyboard() to loginBtn in the viewDidLoad() of my LoginVC like so:
loginBtn.bindToKeyboard()
The problem here is, after the first tap to the textfield (either email or password field), the button disappears. After the keyboard is closed, the button is actually back to its initial position just like in the first picture. Then calling the keyboard again by tapping one of those textfields, the button works properly. But the second and so forth tap, it does not.
The point of my question:
how can I implement the extension to be able to work properly with multiple textfields/textviews?
If that's not possible, how should I approach this problem?
I am sorry if my explanation and or English is unclear.
Thank you so much.
In this animation, if you use frame to control the position of button, the button is supposed to be free of constrains in vertical direction.
UIView.animateKeyframes(withDuration: duration, delay: 0.0, options: UIView.KeyframeAnimationOptions(rawValue: curve), animations: {
self.frame.origin.y += deltaY
}, completion: nil)
I use this animation well after removing all the constraints from UIButton. Otherwise, self.frame.origin.y += deltaY should be replaced with constraint constant.
Good lucky with moving buttons.
I had the exact same problem and initially I tried handling it via the UIKeyboardNotification method but the problem was when the different UITextField was being edited when the Keyboard had changed its state, it won't register for any state change as it was already active. Therefore after much exploring I handled it via creating an accessoryView with a new button that is a duplication of my actual button. So basically, consider you have a UIButton which is sticking at the bottom of the UIViewController and you have 2 UITextField which when switched interchangeably, cannot feel that the UIButton was ever moved elsewhere but remain stuck on the keyboard. This following piece of code explains how to cater this problem:
#IBOutlet weak var signInBtn: UIButton!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var emailTextField: UITextField!
var accessoryViewKeyboard:UIView?
var btnAccessory:UIButton?
override func viewDidLoad() {
super.viewDidLoad()
passwordTextField.delegate = self
emailTextField.delegate = self
accessoryViewKeyboard = UIView(frame: signInBtn.frame)
//Inputting the "accessoryViewKeyboard" here as the "inputAccessoryView" is of
//utmost importance to help the "signInBtn" to show up on tap of different "UITextFields"
emailTextField.inputAccessoryView = accessoryViewKeyboard
passwordTextField.inputAccessoryView = accessoryViewKeyboard
setupBtnWithKeyboard()
}
func setupBtnWithKeyboard() {
btnAccessory = UIButton(frame: CGRect(x: signInBtn.frame.origin.x, y: signInBtn.frame.origin.y, width: self.view.frame.size.width, height: signInBtn.frame.size.height))
accessoryViewKeyboard?.addSubview(btnAccessory!)
btnAccessory?.translatesAutoresizingMaskIntoConstraints = false
btnAccessory?.frame = CGRect(x: (accessoryViewKeyboard?.frame.origin.x)!,
y: (accessoryViewKeyboard?.frame.origin.y)!,
width: self.view.frame.size.width,
height: (accessoryViewKeyboard?.frame.size.height)!)
btnAccessory?.backgroundColor = UIColor(red: 31/255, green: 33/255, blue: 108/255, alpha: 1)
btnAccessory?.setTitle("Sign In", for: .normal)
btnAccessory?.titleLabel?.font = .systemFont(ofSize: 22)
btnAccessory?.titleLabel?.textColor = UIColor.white
btnAccessory?.titleLabel?.textAlignment = .center
btnAccessory?.isEnabled = true
btnAccessory?.addTarget(self, action: #selector(SignIn.signInBtnPressed), for: .touchUpInside)
NSLayoutConstraint.activate([
btnAccessory!.leadingAnchor.constraint(equalTo:
accessoryViewKeyboard!.leadingAnchor, constant: 0),
btnAccessory!.centerYAnchor.constraint(equalTo:
accessoryViewKeyboard!.centerYAnchor),
btnAccessory!.trailingAnchor.constraint(equalTo:
accessoryViewKeyboard!.trailingAnchor, constant: 0),
btnAccessory!.heightAnchor.constraint(equalToConstant: signInBtn.frame.size.height),
])
}
And you're done. This will keep the UIButton always present on the Keyboard. Important thing is no matter how many instances of UITextField you introduce, always input the accessoryViewKeyboard as its inputAccessoryView.
I have an textview at bottom of screen and search bar at top of screen. Following is my code to solve the problem of keyboard when textview is pressed
extension UIView {
func bindToKeyboard(){
NotificationCenter.default.addObserver(self, selector: #selector(UIView.keyboardWillChange(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
#objc func keyboardWillChange(_ notification: NSNotification) {
let duration = notification.userInfo![UIKeyboardAnimationDurationUserInfoKey] as! Double
let curve = notification.userInfo![UIKeyboardAnimationCurveUserInfoKey] as! UInt
let curFrame = (notification.userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let targetFrame = (notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let deltaY = targetFrame.origin.y - curFrame.origin.y
UIView.animateKeyframes(withDuration: duration, delay: 0.0, options: UIViewKeyframeAnimationOptions(rawValue: curve), animations: {
self.frame.origin.y += deltaY
},completion: {(true) in
self.layoutIfNeeded()
})
}
}
But when I press search bar then the screen moves up and search bar disappears. If I do view.bindToKeyboard() then the edittext is proper after displaying the keyboard.
One solution which I tried was binding the outlet of textview to keyboard but the textview disappears as soon as I start typing.
I think the problem is that you are trying to know when the keyboard is going to appear. Searchbar has a textfield. So when it's tapped it opens the keyboard like your textview and the keyboardWillChange is called.
keyboardWillChange(_ notification: NSNotification)
So the keyboard appears and hides your searchbar. You can detect searchbar tap and cancel the notification there.
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool
{
//Dismiss your keyboard notification here
return true
}
You should use a library to handle this situation. I'm suggesting IQKeyboardManager. IQKeyBoardManager Github.
I edit my earlier respond... have you try to put your searchBar into navigationBar?
something like this (just put on viewDidLoad):
navigationItem.titleView = searchBar