I have an UITextField inside of a container view, when the keyboard pops up the container view disappears. In the same container view is a UICollectionView, whose custom cells each contains a UITextField and the keyboard works just fine for them.
I printed out the frame of the container view in the animation function that is called by keyboardWillShow and the container view's frames are the same for both cases, so it looks like the container view just disappears (instead of "not moved" as i thought) when that specific UITextField is selected. The relevant code is :
func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
if let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
kbHeight = keyboardSize.height
self.animateDurationView(true)
}
}
}
func keyboardWillHide(notification: NSNotification) {
self.animateDurationView(false)
}
func animateDurationView(up: Bool) {
var movement = up ? -kbHeight : kbHeight
println(movement)
UIView.animateWithDuration(0.3, animations: {
self.durationView.frame = CGRectOffset(self.durationView.frame, 0, movement)
println(self.durationView.frame)
})
}
the screenshots can be found in this thread: KeyboardWillShow only moves container UIView for certain UITextFields
EDIT: at this point I am almost certain that it is the auto layout constraints that are screwing with me.
Try adding commitAnimations at the end of each animation transaction :) Me too I faced this issue... Now it is working fine.. You can add delegate to the textfield and you can move it delegate methods DidBeginEditing and DidEndEditing methods for textfield :)
Just had the same issue. What I did wrong was trying to apply the movement to an inner view. Moving the outermost view made it work as this outer view does not have any auto layout constraints.
So to answer the question - if anyone else should get here
Only move the self.view
Related
I'm trying to create a page in an app that's your standard style messaging screen. I'm having trouble getting everything to position correctly when the keyboard slides into view. I'll post screenshots (sadly not inline), but here is my structure:
VIEWCONTROLLER
|-View
|-Scroll View
|-Content View
|-TextField
|-TableView (messages)
Everything is showing up as I would like it to when first loaded: If there aren't enough messages to fill the screen, the messages start at the top followed by a gap, and the text field is pinned to the bottom. Nothing scrolls. If there are a lot of messages, I am successfully scrolling the table to the last row and the textfield is pinned to the bottom of the screen still.
When the textfield is activated however, and there aren't a lot of messages, the gap between the table and the textfield remains and the messages are pushed out of view to the top.
I am trying to get the gap to shrink so the messages stay. This is standard in other messaging apps, but I cannot figure out how to do it
Initial view
Textfield activated, keyboard appears
Scrolling to display messages hides the textfield
UI Layout and constraints
Lastly, here is the code I have for keyboardWillShow. You'll notice some comments of things I have tried unsuccessfully.
func keyboardWillShow(notification:NSNotification) {
var userInfo = notification.userInfo!
let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardFrame!.height, 0.0)
self.scrollView.contentInset = contentInsets
self.scrollView.scrollIndicatorInsets = contentInsets
// scrollViewBottomConstraint.constant = keyboardFrame!.height - bottomLayoutGuide.length
// contentViewHeightConstraint.constant = -keyboardFrame!.height
// self.notificationReplyTable.frame.size.height -= keyboardFrame!.height
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardFrame!.height
if let activeField = self.activeField {
if(!aRect.contains(activeField.frame.origin)) {
self.scrollView.scrollRectToVisible(activeField.frame, animated: true)
}
}
}
I feel like the piece I'm missing is pretty small, but just don't know enough Swift 3 to nail this. Thank you for your help!
Edit: the problem is similar to this question with no accepted answer.
A way to this is to set up vertical autolayout constraints like this (but you will need a reference to the actual bottomMargin constraint to be able to modify it) :
"V:|[scrollView][textField]-(bottomMargin)-|"
The first time you arrive on the screen, bottomMargin is set to 0.
Then when keyboardWillShow is called, get the keyboard frame (cf How to get height of Keyboard?)
func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
}
}
And animate the constraint bottomMargin to get the height of the keyboard (the duration is 0.3 after some tests, but you can adjust it) :
bottomConstraint.constant = keyboardHeight
UIView.animate(withDuration: 0.3, delay: 0, options: nil, animations: {
self.view.layoutIfNeeded()
}
That means that every time the keyboard will appear, an animation will move up the text field, hence the scroll view height will be smaller and everything will fit in the screen.
!! Don't forget to test it on landscape mode if you support it, and on iPad too!!
Finally, handle the case when the keyboard will disappear in the keyboardWillHide and set bottomMargin back to 0 :
func keyboardWillHide(_ notification: Notification) {
bottomConstraint.constant = 0
UIView.animate(withDuration: 0.3, delay: 0, options: nil, animations: {
self.view.layoutIfNeeded()
}
}
My view has a TextView with 0 as the number of lines, populated with a long text that the user can edit.
When the keyboard shows, to resize the TextView so that it fits in the visible portion of the screen, the height constraint of an empty view at the bottom of the screen is equaled to the keyboard height. The bottom of the textView is constrained to the top of the empty view, so the text view gets resized.
When this happens, the textView automatically scrolls down in the text. I would like the text to avoid this scrolling, so that the first line always stays visible. I have tried a few ways, such as:
Disabling scrolling between keyboardWillShow and keyboardDidShow, not working
Scrolling back to zero, but we can see the text scrolling down and then scrolling back up (textView.scrollRangeToVisible(NSRange(location:0, length:0)))
Here's a drawing to make it much clearer (can't embed it yet, sorry):
UITextView Drawing
Relevant code:
func keyboardWillShow(_ notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let duration: TimeInterval = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
emptyViewHeightConstraint.constant = keyboardSize.height
UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}
}
func keyboardWillHide(_ notification: NSNotification) {
let duration: TimeInterval = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
emptyViewHeightConstraint.constant = 0
UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}
PS: The TextView is actually inside a StackView, and this StackView has its bottom constrained to the top of the empty view.
I understand that the issue I'm posting has been discussed a lot but as far as I have searched in SO, I couldn't find a solution specific to my issue. I'm posting this to get some inputs.
Problem:
View hierarchy: View -> ScrollView -> View -> Textfield
I have implemented two screens with multiple textfields laid on top of a scrollview to avoid keyboard obscuring the textfield when its at the bottom of the page. I have achieved this using the sample code provided by Apple with minor modifications.
Subscribe to UIKeyboardWillShowNotification and UIKeyboardWillHideNotification
When keyboard is shown, get the height of the keyboard from userInfo dictionary and set appropriate content inset to move the textfield above the Keyboard
When keyboard is hidden, set the content inset to UIEdgeInsetZero to bring back to normal size
The above implementation works perfectly fine when there are more textfields that could occupy the entire screen (assume there are 10 textfields and they extend beyond the frame of the the scroll view). Whenever I tap on a textfield, it moves above the keyboard as expected and I can also scroll the page till the bottom most textfield is visible above the keyboard frame (when the keyboard is still up).
My problem arises when I have only two textfield in the scrollview that are centered vertically in the screen. When I tap on a textfield, the scrollview get an inset equivalent to the keyboard height and it moves above the keyboard as expected but when I scroll the screen, I could see a huge blank space (due to the additional inset) below the textfields.
How can I avoid that blank space when there are only one or two textfields in the page? Should I have to write a different logic to handle that scenario? Any help would be appreciated.
Below is the code:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue()
if let kbFrame = keyboardFrame {
let inset = CGRectGetHeight(kbFrame)
scrollView.contentInset = UIEdgeInsetsMake(0, 0, inset, 0)
scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, inset, 0)
}
}
}
func keyboardWillHide(notfication: NSNotification) {
scrollView.contentInset = UIEdgeInsetsZero
scrollView.scrollIndicatorInsets = UIEdgeInsetsZero
}
TPKeyboardAvoidingScrollView is good, but i recently switched to IQKeyboardManager -> https://github.com/hackiftekhar/IQKeyboardManager
Its upto you to choose but i prefer IQKeyboardManager cause its easy
There are a number of solutions out there, but my preferred solution is to use TPKeyboardAvoiding. To use it here you would just make your UIScrollView an instance or subclass of TPKeyboardAvoidingScrollView in SB or code and it should care of the rest, no other code required!
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
}
I have editable UITextView and keyboard dismiss mode is interactive. Also my controller is listening two notifications: UIKeyboardWillShowNotification, UIKeyboardWillHideNotification.
func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
var insets = self.textView.contentInset;
let rect = userInfo[UIKeyboardFrameEndUserInfoKey]?.CGRectValue() ?? CGRectZero
insets.bottom = (rect.size.height - (CGRectGetHeight(self.view.frame) - CGRectGetMaxY(self.textView.frame)))
self.textView.contentInset = insets
self.textView.scrollIndicatorInsets = insets
}
}
func keyboardWillHide(notification: NSNotification) {
self.textView.contentInset = UIEdgeInsetsZero
self.textView.scrollIndicatorInsets = UIEdgeInsetsZero
}
This stuff works great, if text in UITextView doesn't contain any empty lines. If it do, contentOffset jumps to another, random place.
I'm not sure if this is a bug in iOS 7+, or I am doing something wrong.
If it's not a bug, how to get this going fluently without the jumping behaviour?
Thanks for your help.
I had been battling this exact same problem, when I would dismiss the keyboard the UITextView's content offset would jump back to {0, 0}. Interestingly, I only got this behavior on the device, but not in the simulator.
I originally tried to solve it by overriding UITextView's contentOffset method and having it just ignore {0, 0} values, and that was semi effective, until the content got too long, in which case it would just jump to a random offset, and set the same value 3 times (so it would set content offset to {0, 3605}, {0, 3605}, and {0, 3605} all in rapid succession).
After a long time spent looking for a solution, it turned out to be rather simple:
textview.layoutManager.allowsNonContiguousLayout = NO;
As discussed in this blog post. Hope that helps :)
I had 100% exactly the same problem as you and I also asked a question about it but no one could get it right. (I am the one who up voted and favourited your question!!)
I eventually did a workaround after 4 days of frustration. Just put the UITextView inside a UITableView (You don't need to put it inside a UITableViewCell, just drag to the UITableView then it's ok). Make your UITextView unscrollable.
The following method will make UITextView expand and update the UITableView every time it is changed. (Don't forget to connect UITextView's delegate)
func textViewDidChange(textView: UITextView) {
// Change textView height
self.textView.sizeToFit()
UIView.setAnimationsEnabled(false)
self.tableView.beginUpdates()
self.tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
The following method will make UITableView autoscroll to the cursor when UITextView becomes active.
func textViewDidBeginEditing(textView: UITextView) {
// Delay the following line so that it works properly
let delay = 0.005 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
var rect = self.textView.caretRectForPosition(self.textView.selectedTextRange?.end)
var changedRect = CGRectMake(rect.origin.x, rect.origin.y, rect.width, rect.height+3)
self.tableView.scrollRectToVisible(changedRect, animated: true)
}
}
You also need to change the UITableView contentInset and scrollIndicatorInsets in your keyboardWillShow and keyboardWillHide methods, depending on your screen layout.