I am using UITextField hosted inside UIScrollView and using keyboard notification to adjust content size, right now when scrollview scrolls to textfield the padding between keyboard and textfield is too small. Is there anyway I can customise padding??
You can use IQKeyboardManager for automatically provide padding and without any trouble of scrollView with single Line of code. You can install it via CocoaPods or Manually.
Set the contentInset of the UIScrollView
scrollView.contentInset = .init(top: 0, left: 0, bottom: 15, right: 0)
add bottom constraint outlet of Scroll View( this must be the bottom constraint of last textfield from bottom).
and in that view controller add the following line of code.
fileprivate func addKeyBoardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
// self.view.frame.origin.y -= keyboardSize.height
self.scrollViewBottomAnchor.constant = -keyboardSize.height
self.view.layoutIfNeeded()
})
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.scrollViewBottomAnchor.constant = 0
self.view.layoutIfNeeded()
})
}
}
Related
Although I've searched, I'm confused about how best to approach this.
I have a tableView where the bottom cell is an input to the list, in the same way apple reminders work. When there are too many items on the list, the keyboard covers the list and I can't see what I'm typing.
My thought it I need to change the physical size of the table view and ensure it is scrolled to the bottom when the keyboard shows.
Some have said keyboard observers but the majority of code I've found for this is out of date and errors when put into Xcode.
NSNotificationCenter Swift 3.0 on keyboard show and hide
This is the best I can find but I'm also hearing about contraints, using a UITableViewController instead of embedding a UITableView and so on ...
This is the code I have so far:
NotificationCenter.default.addObserver(self, selector: #selector(EntryViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(EntryViewController.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
#objc func keyboardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
print("notification: Keyboard will show")
if self.view.frame.origin.y == 0{
self.view.frame.origin.y -= keyboardSize.height
}
}
}
#objc func keyboardWillHide(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y != 0 {
self.view.frame.origin.y += keyboardSize.height
}
}
}
This moves the whole view up I think, which means safeareas (such as navigation bar and so on) have the TableView underneath them. Do I make the navigationview non-transparent in this approach?
One solution (which I sometimes use) is simply change the content offset of the tableView when the keyboard appears/disappears. I believe this would work in your instance as opposed to varying the tableView's constraints as you mentioned your UIViewController is a UITableViewController. Please see the below code for my suggestion, hope this helps!
Handle Notifications:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(EntryViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(EntryViewController.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
Actions:
#objc func keyboardWillShow(notification: Notification) {
if let keyboardHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height {
print("Notification: Keyboard will show")
tableView.setBottomInset(to: keyboardHeight)
}
}
#objc func keyboardWillHide(notification: Notification) {
print("Notification: Keyboard will hide")
tableView.setBottomInset(to: 0.0)
}
Extensions:
extension UITableView {
func setBottomInset(to value: CGFloat) {
let edgeInset = UIEdgeInsets(top: 0, left: 0, bottom: value, right: 0)
self.contentInset = edgeInset
self.scrollIndicatorInsets = edgeInset
}
}
At the bottom of the screen,I have a stackView with a TextView inside it.TextView will becomeFirstResponder() in viewDidLoad().What I want is,the keyboard is shown below the stackView as I mention.
Here is my code :
override func viewDidLoad() {
super.viewDidLoad()
textView.becomeFirstResponder()
self.hideKeyboardWhenTappedAround() //here will hide the keyboard
NotificationCenter.default.addObserver(self, selector: #selector(MyViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(MyViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if let window = self.view.window?.frame {
// We're not just minusing the kb height from the view height because
// the view could already have been resized for the keyboard before
self.view.frame = CGRect(x: self.view.frame.origin.x,
y: self.view.frame.origin.y,
width: self.view.frame.width,
height: window.origin.y + window.height - keyboardSize.height)
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let viewHeight = self.view.frame.height
self.view.frame = CGRect(x: self.view.frame.origin.x,
y: self.view.frame.origin.y,
width: self.view.frame.width,
height: viewHeight + keyboardSize.height)
}
}
After apply the code above,the Keyboard is shown.The stackView I mention is above the keyboard for 3 seconds.Then the stackView at the bottom disappear.Even when I hide the keyboard,the stackView also never shown out.
I totally have no idea what is happening.Somebody please kindly explain whats going wrong with my code here
You can try
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
self.viewBotCon.constant = -1 * keyboardSize.height
self.view.layoutIfNeeded()
}
}
#objc func keyboardWillHide(notification: NSNotification) {
self.viewBotCon.constant = 0
self.view.layoutIfNeeded()
}
Try to use autolayout as much as possible. It's better to treat frame and bounds property as readonly.
The best way to do it actually
setup constraint from bottom of stack view to safe area, and expose it to you code via IBOutlet.
I advice you to subscribe for UIKeyboardWillChangeFrame - it then you will able to react on changes in keyboard as well.
Use window coordinate space to calculate all intersections with keyboard: let stackViewFrame = stackView.convert(stackView.bounds, to: nil)
Calculate intersection between keyboard and you stackview: let intersection = keyboardFrame.intersection(stackViewFrame)
Adjust constant of your constraint to intersection + offset: constraint.constant = intersection.size.height + offset
Get animation time from notification let duration = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey].floatValue
and update layout with animation: UIView.animate(withDuration: duration) {self.view.layoutIfNeeded() }
PS: You subscribing for notifications in viewDidLoad and never unsubscribing from them. It means that if you have any other subsequent Scenes your viewcontroller will react on them as well, consuming performance and probably causing defects. It's better practice to subscribe for keyboard notifications in viewWillAppear and unsubscribe in viewDidDisappear.
PSS: And it will be good for your skills to use subscription with blocks rather then selectors.
I want to move the screen upwards when I show the keyboard and move to the bottom when I hide the keyboard.
I use this code:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y == 0{
self.view.frame.origin.y -= keyboardSize.height
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y != 0{
self.view.frame.origin.y += keyboardSize.height
}
}
}
But I have this problem: http://gph.is/2AvLo1A
On this screen I tap on textField to show keyboard and tap on empty space to hide keyboard several times. My screen crawls down, and the black background fills the entire screen. How to fix it?
Update:
http://gph.is/2F7nux5
After shown the keyboard I have new empty space at the bottom. And if I scroll I can see it: https://imgur.com/a/fFyEC
Hook the bottom constraint of the scrollview to the superViews's bottom and drag it as IBOutlet and do this
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
self.scrollviewBottomCon.constant = keyboardSize.height
self.view.layoutIfNeeded()
self.scrollView.scrollRectToVisible((self.textfeild.frame), animated: true)
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
self.scrollviewBottomCon.constant = 0
self.view.layoutIfNeeded()
}
}
Try not to move the entire view up and down, rather the components you really need to move. I'd suggest using a view to contain the elements you want to move.
I have a use case where I have a coupe text fields and a submit button beneath in a container view. When the keyboard appears, I wanted the two text fields and button to rise. Here's how I handled that.
#objc func keyboardWillAppear(_ notification: NSNotification) {
if let userInfo = notification.userInfo,
let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
// Calculate how much to move
let submitButtonHeight = self.submitButton.frame.size.height
var submitButtonOrigin = self.view.convert(self.submitButton.frame.origin, from: self.containerView)
submitButtonOrigin.y = submitButtonOrigin.y + submitButtonHeight + 4.0
var visibleRect = self.view.frame
visibleRect.size.height -= keyboardFrame.height
// If keyboard covers the button, move the container up
if !visibleRect.contains(submitButtonOrigin) {
let keyboardMove = submitButtonOrigin.y - visibleRect.size.height
if keyboardMove > 0 {
UIView.animate(withDuration: 0.3) {
self.containerView.transform = CGAffineTransform(translationX: 0, y: -keyboardMove)
}
}
}
}
}
#objc func keyboardWillDisappear(_ notification: NSNotification) {
UIView.animate(withDuration: 0.3) {
self.containerView.transform = CGAffineTransform.identity
}
}
This may or may not be a solution for you, but can provide another option.
I am trying to slide a textbox, which is located in the bottom of a tableview (actually, in its footer view). So I tried two methods:
To animate autolayout constraint
Animate content insets of a tableview
Still, non of them worked, or partially worked. Currently, I am not able to test this on a real iPad (app is iPad only) and I am testing on Simulator, but I thought something like this should work:
Animating contentInsets
func keyboardWillShow(notification: NSNotification) {
if let keyboardHeight = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height {
UIView.animate(withDuration: 0.2, animations: {
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0)
})
}
}
func keyboardWillHide(notification: NSNotification) {
UIView.animate(withDuration: 0.2, animations: {
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
})
}
Animating autolayout constraint
func keyboardWillShow(notification: NSNotification) {
if let keyboardHeight = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height {
UIView.animate(withDuration: 0.2, animations: {
self.bottomTableviewConstraint.constant = keyboardHeight
self.qaTableview.layoutIfNeeded()
})
}
}
func keyboardWillHide(notification: NSNotification) {
UIView.animate(withDuration: 0.2, animations: {
self.bottomTableviewConstraint.constant = 0
self.qaTableview.layoutIfNeeded()
})
}
So in first example, I never noticed animation, nor tableview content moving. In the second example I made an outlet of a autolayout constraint (bottom vertical spacing) but I get some odd results. eg, tableview moves up, but not completely, and only for the first time.
If I put a breakpoint, keyboardHeight is equal to 471. I am I missing something obvious ?
This is what I tried and it worked perfectly fine as expected.
Can you check where you added your Notification center?
-- Animating contentInsets
override func viewWillAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
if let keyboardHeight = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height {
UIView.animate(withDuration: 0.2, animations: {
self.tbl.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0)
})
}
}
func keyboardWillHide(notification: NSNotification) {
UIView.animate(withDuration: 0.2, animations: {
self.tbl.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
})
}
-- Animating autolayout constraint
Make sure you do not have both Bottom layout and height constraints set for UITableView in Storyboard.
NOTE
But there's an even more easy way to do this, just by using a UITableViewController. It will take care of this feature by default. This is, of course, if you have no problem in using it.
EDIT
UITextView will act a little different from UITextField. Set delegate for UITextView. And do all table view alignments in textViewShouldBeginEditing.
extension ViewController: UITextViewDelegate {
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
let pointInTable:CGPoint = textView.superview!.convert(textView.frame.origin, to: tbl)
tbl.contentOffset = CGPoint(x: tbl.contentOffset.x, y: pointInTable.y)
return true
}
}
I use IQKeyboardManager library, which handles this behavior: https://github.com/hackiftekhar/IQKeyboardManager
IQKeyboardManager allows you to prevent issues of the keyboard sliding up and cover UITextField/UITextView without needing you to enter any code and no additional setup required. To use IQKeyboardManager you simply need to add source files to your project.
It's really easy to implement. Import the library with pods, and just add, in your AppDelegate:
import IQKeyboardManager
And in didFinishLaunchingWithOptions:
IQKeyboardManager.sharedManager().enable = true
I need to move a UIView up as soon as the keyboard will become visible. But the problem I'm facing right now is that my UIKeyboardWillShowNotification is called three times when I'm using a custom Keyboard (e.g. SwiftKey) which results in a bad animation.
Is there a way to handle only the last notification? I could easily dodge the first one because the height is 0, but the second one looks like a valid height and I don't find an answer on how to solve this.
Here is what I've so far:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillAppear:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillDisappear:", name: UIKeyboardWillHideNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillAppear(notification: NSNotification){
print("keyboard appear")
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() {
print("with height: \(keyboardSize.height)")
if keyboardSize.height == 0.0 {
return
}
self.txtViewBottomSpace.constant = keyboardSize.height
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
}
func keyboardWillDisappear(notification: NSNotification){
print("Keyboard disappear")
self.txtViewBottomSpace.constant = 0.0
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
My Log output is:
keyboard appear
with height: 0.0
keyboard appear
with height: 216.0
keyboard appear
with height: 258.0
Keyboard disappear
So is there any way to only handle the third notification and "ignore" the first two?
Set all bellow fields to NO can resolve this problem.
Capitalizaion: None
Correction: No
Smart Dashes: No
Smart insert: No
Smart Quote: No
Spell Checking: No
Change the notification name UIKeyboardDidShowNotification and UIKeyboardDidHideNotification then solve the problem
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillAppear:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillDisappear:", name: UIKeyboardDidHideNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillAppear(notification: NSNotification){
print("keyboard appear")
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() {
print("with height: \(keyboardSize.height)")
if keyboardSize.height == 0.0 {
return
}
self.txtViewBottomSpace.constant = keyboardSize.height
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
}
func keyboardWillDisappear(notification: NSNotification){
print("Keyboard disappear")
self.txtViewBottomSpace.constant = 0.0
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
I suggest to replace the static animation duration (0.4) with the animation duration of the keyboard, returned in the userInfo dictionary of the notification under UIKeyboardAnimationDurationUserInfoKey.
In this way your animation will be in sync with the keyboard animation. You can also retrieve the animation curve used by the keyboard with the UIKeyboardAnimationCurveUserInfoKey key.
func keyboardWillAppear(notification: NSNotification){
print("keyboard appear")
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() {
let animationDuration = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey]?.doubleValue;
print("with height: \(keyboardSize.height)")
if keyboardSize.height == 0.0 {
return
}
self.txtViewBottomSpace.constant = keyboardSize.height
UIView.animateWithDuration(animationDuration!, delay: 0.0, options: .BeginFromCurrentState, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
}
func keyboardWillDisappear(notification: NSNotification){
print("Keyboard disappear")
let animationDuration = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey]?.doubleValue;
self.txtViewBottomSpace.constant = 0.0
UIView.animateWithDuration(animationDuration!, delay: 0.0, options: .BeginFromCurrentState, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
The reason for this is because keyboards can have different sizes, especially third party ones. So the first notification you receive will always be for the default system height, and any you receive after that will include the new heights of a third party keyboard extension if one is loaded. In order to get around this, in your method that handles the notification, you need to get the height originally, and then set that as a default height (I think 226). Then set a variable to this first height, and then for resulting calls to the notification method you can check if the new height is greater than the original height, and if it is you can find the delta, and then readjust your frames accordingly.