Scrolling tableview when keyboard appears for UITextView - ios

I know this is an often asked question, but I cannot get any of the posted solutions (e.g. here) to work properly in my case. First off, when I handle keyboardWillShow notification using
if let keyboardHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height {
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
}
nothing happens. It seems like a bit of a hack, but this implementation below (specifically in keyboardWillShow) works for me, however strangely (1) it doesn't work the first time but works every subsequent time also (2) a large white bar appears above the keyboard for some reason? I don't think it matters but my UI lets the user tap an edit button so they can see what is editable, then they edit the textView, then tap done. The problem I'm trying to solve is that this textView is at the bottom of the tableView, so the keyboard obscures it while editing.
class ScoreAndStatsViewController: UITableViewController, UITextFieldDelegate, UITextViewDelegate {
#IBOutlet weak var editButton: UIButton!
#IBOutlet weak var notesTextField: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIApplication.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIApplication.keyboardWillHideNotification, object: nil)
}
#IBAction func editButtonIsPressed(_ sender: UIButton) {
if editMode == false {
notesTextField.isEditable = true
notesTextField.backgroundColor = iPhoneForeGroundColor
editButton.setTitle("Done", for: .normal)
self.editMode = true
//If edit mode is true, this means they've hit the done button so save
} else {
//save data
editButton.setTitle("Edit", for: .normal)
notesTextField.isEditable = false
notesTextField.backgroundColor = UIColor.clear
self.editMode = false
}
}
// MARK: Keyboard Notifications
#objc func keyboardWillShow(notification: NSNotification) {
let pointInTable:CGPoint = notesTextField.superview!.convert(notesTextField.frame.origin, to: tableView)
var contentOffset:CGPoint = tableView.contentOffset
contentOffset.y = pointInTable.y
if let accessoryView = tableView.inputAccessoryView {
contentOffset.y -= accessoryView.frame.size.height
}
tableView.contentOffset = contentOffset
}
#objc func keyboardWillHide(notification: NSNotification) {
UIView.animate(withDuration: 0.2, animations: {
// For some reason adding inset in keyboardWillShow is animated by itself but removing is not, that's why we have to use animateWithDuration here
self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
})
}
}

Related

Resize UITextView when keyboard appears

I try to resize the height of a UITextView-field when the keyboard appears (iOS 14.2, Xcode 12.3). The distance from the bottom of the UITextView to the save area is 90 and hence the lower part of it is hidden by the keyboard and can't be seen while editing.
I tried it with the solution shown here: Resize the UITextView when keyboard appears
Accordingly, my code is as follows:
class EditierenVC: UIViewController, UITextFieldDelegate, UITextViewDelegate {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(false)
NotificationCenter.default.addObserver(
self,
selector: #selector(EditierenVC.handleKeyboardDidShow(notification:)),
name: UIResponder.keyboardDidShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(EditierenVC.handleKeyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}
#objc func handleKeyboardDidShow(notification: NSNotification) {
guard let endframeKeyboard = notification
.userInfo![UIResponder.keyboardFrameEndUserInfoKey]
as? CGRect else { return }
textfeld.contentInset = UIEdgeInsets(
top: 0.0,
left: 0.0,
bottom: endframeKeyboard.size.height-85,
right: 0.0
)
view.layoutIfNeeded()
}
#objc func handleKeyboardWillHide() {
textfeld.contentInset = .zero
view.layoutIfNeeded()
}
//**************************
// MARK: - Views
//**************************
#IBOutlet weak var textfeld: UITextView!
Unfortunately the inset-size is not changed, when the keyboard appears and the text is partly hidden.
Has anyone an idea, why it is not working?
Thanks for your support
I could not come up with a solution by using content inset but I can suggest another way.
If you add bottom constraint to the textView and create outlet for that, you can change its constant value in notifications;
#objc func handleKeyboardDidShow(notification: NSNotification) {
guard let endframeKeyboard = notification
.userInfo![UIResponder.keyboardFrameEndUserInfoKey]
as? CGRect else { return }
textViewBottomConstraint.constant = endframeKeyboard.size.height-85
}
#objc func handleKeyboardWillHide() {
textViewBottomConstraint.constant = // set the previous value here
}

How to make scrollView scrolling only if keyboard appears in swift

I am using scrolview for view with height 1000, initially i don't want my scrolView to scroll. if i tap on any textField then i want my scrolview to scroll and if i return keyboard then i don't want my scrollview to scroll.
Here i am able to textfield up when when keyboard appears but i am unable to return textfield to its orginal position when i return keyboard and when i return keyboard i dont want my view to scroll,
Please help me in the code.
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var scrolView: UIScrollView!
#IBOutlet weak var upTFLD: UITextField!
var activeTextField = UITextField()
#IBOutlet weak var downTFLD: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
upTFLD.delegate = self
downTFLD.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardAppear(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardDisappear(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
}
#objc func onKeyboardAppear(_ notification: NSNotification) {
let info = notification.userInfo!
let rect: CGRect = info[UIResponder.keyboardFrameBeginUserInfoKey] as! CGRect
let kbSize = rect.size
let insets = UIEdgeInsets(top: 0, left: 0, bottom: kbSize.height+20, right: 0)
self.scrolView.contentInset = insets
self.scrolView.scrollIndicatorInsets = insets
var visibleRect: CGRect = self.scrolView.convert(self.scrolView.bounds, to: self.view)
visibleRect.size.height -= rect.size.height;
let inputRect: CGRect = self.activeTextField.convert(self.activeTextField.bounds, to: self.scrolView)
if (visibleRect.contains(inputRect)) {
self.scrolView.scrollRectToVisible(inputRect, animated: true)
}
}
#objc func onKeyboardDisappear(_ notification: NSNotification) {
self.scrolView.contentInset = UIEdgeInsets.zero
self.scrolView.scrollIndicatorInsets = UIEdgeInsets.zero
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
activeTextField = textField
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
activeTextField.resignFirstResponder()
return true
}
}
initially i dont want scrolling, and if i return keyboard i need textfield come to its original position and no scrolling again.
Only keyboard appears then only i need scrolling. please help me in the code.
You just need to set the contentOffset of your ScrollView right after the keyboard is hidden.
Create a variable to store offsetBeforeShowKeyboard
var offsetBeforeShowKeyboard: CGFloat?
When view is initially loaded:
self.scrollView.isScrollEnabled = false
When select any TextField:
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.scrollView.isScrollEnabled = true
if (self.offsetBeforeShowKeyboard == nil) {
self.offsetBeforeShowKeyboard = self.scrollView.contentOffset
}
}
When keyboard is hidden
#objc func onKeyboardDisappear(_ notification: NSNotification) {
self.scrollView.isScrollEnabled = false
if let offset = self.offsetBeforeShowKeyboard {
self.scrolView.setContentOffset(offset, animated: true)
}
self.offsetBeforeShowKeyboard = nil
}
In viewWillAppear
yourScrollview.isScrollEnabled = false
After Keyboard appears make it true
yourScrollview.isScrollEnabled = true
Alternatively you can use IQKeyboard manager to take care of textfields.Checkout: IQKeyboardManager

Clicking on second UITextField with keyboard already displayed moves content incorrectly

EDIT: Added the methods that were requested.
UPDATE: When I click on input 1 the keyboardwillchange callback is called once. When I click on the 2nd input without first dismissing the keyboard, the keyboardwillchange callback is called TWICE for some reason. Additionally, I noticed my code is still calculating the correct frames. However, it seems xcode is ignoring that or just overriding and putting things back to how they were originally constructed.
I have a screen with some image content at the top and 2 text fields and a text view at the bottom half. The inputs are in a stack view. Whenever I click on a text field/view I move the stack view up far enough so all inputs are visible. When I dismiss the keyboard the stack view goes back to it's original location. This works great! When I click on one text field (regular keyboard) and then click on the other text field (number pad) my stack view goes back to the original location when it shouldn't move at all. To try and fix this I put in some boolean that tracks if the keyboard is displayed and use it to prevent my UIKeyboardWillShow method from doing anything.
What is Xcode 9.1/Swift 4 doing that I can stop from happening?
Side note: I tried using IQKeyboardManager, but it doesn't seem to allow me to prevent the top image content from moving too much.
My relevant code:
class MoodEntryVC: UIViewController, UITextFieldDelegate, UITextViewDelegate {
#IBOutlet weak var questionsStackView: UIStackView!
#IBOutlet weak var hoursTextField: UITextField!
#IBOutlet weak var locationTextField: UITextField!
#IBOutlet weak var otherInfoTextView: UITextView!
#IBOutlet weak var headerBarView: UIView!
#IBOutlet weak var bgHeaderView: UIImageView!
var currentTextField: UITextField?
var currentTextView: UITextView?
var currentBGHeaderFrame: CGRect!
var currentQuestionsStackFrame: CGRect!
var currentFieldEditing: FieldType!
var keyboardVisible: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
hideKeyboardWhenTappedAround2()
locationTextField.delegate = self
hoursTextField.delegate = self
otherInfoTextView.delegate = self
currentBGHeaderFrame = bgHeaderView.frame
currentQuestionsStackFrame = questionsStackView.frame
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidShow(_:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillChangeFrame(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
fileprivate func moveQuestionsAndBGHeader(_ newTopY: CGFloat, _ bgNewTopY: CGFloat) {
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
let newRect = CGRect(x: self.questionsStackView.frame.origin.x, y: newTopY, width: self.questionsStackView.bounds.width, height: self.questionsStackView.bounds.height)
self.questionsStackView.frame = newRect
self.bgHeaderView.frame = CGRect(x: self.bgHeaderView.frame.origin.x, y: bgNewTopY, width: self.bgHeaderView.bounds.width, height: self.bgHeaderView.bounds.height)
}, completion: nil)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
UIView.animate(withDuration: 1.3, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
textField.resignFirstResponder()
}, completion: nil)
return true
}
#objc func keyboardDidShow(_ notification: NSNotification) {
keyboardVisible = true
}
#objc func keyboardWillChangeFrame(_ notification: NSNotification) {
print("keyboard will change")
}
#objc func keyboardWillShow(_ notification: NSNotification) {
if keyboardVisible {
return
}
// Get keyboard sizing
let endFrame = (notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// Get data entry view sizing
let topY = questionsStackView.frame.origin.y
let bottomY = questionsStackView.frame.height + topY
let newTopY = endFrame.origin.y - 10 - questionsStackView.frame.height
// Get bgHeaderView sizing
let bgTopY = bgHeaderView.frame.origin.y
let bgBottomY = bgHeaderView.frame.height + bgTopY
let gapToDataEntry = topY - bgBottomY
let bgNewTopY = newTopY - gapToDataEntry - bgHeaderView.frame.height
// This moves the stack view to the correct location above the keyboard
moveQuestionsAndBGHeader(newTopY, bgNewTopY)
}
#objc func keyboardWillHide(_ notification: NSNotification) {
UIView.animate(withDuration: 1.3, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.moveQuestionsAndBGHeader(self.currentQuestionsStackFrame.origin.y, self.currentBGHeaderFrame.origin.y)
self.view.endEditing(true)
}, completion: nil)
keyboardVisible = false
}
Thank you!
try adding this method
func textFieldDidEndEditing(textField: UITextField) {
view.endEditing(true)
}
when first textfield will become de active then keyboard will hide. similarly when other textfield will become active then keyboard will show.

Move a view up when the keyboard covers an input field but with leaving some space between them

Description
I have a few UITextField which I want to scroll up, when one of them is covered by keyboard during edition. There are a tons of answers here on SO with many flavors: moving a view (by changing its frame), modifying a constraint, using UIScrollView and UITableView, or using UIScrollView and modifying contentInset.
I decided to use the last one. This one is also described by Apple, and has a Swift version on SO as well as being described on this blog including a sample project on the GitHub.
Partial code
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil)
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldDidEndEditing(textField: UITextField) {
self.activeField = nil
}
func textFieldDidBeginEditing(textField: UITextField) {
self.activeField = textField
}
func keyboardWillShow(notification: NSNotification) {
if let activeField = self.activeField, keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardSize.height, right: 0.0)
self.scrollView.contentInset = contentInsets
self.scrollView.scrollIndicatorInsets = contentInsets
var aRect = self.view.frame
aRect.size.height -= keyboardSize.size.height
if (!CGRectContainsPoint(aRect, activeField.frame.origin)) {
self.scrollView.scrollRectToVisible(activeField.frame, animated: true)
}
}
}
func keyboardWillBeHidden(notification: NSNotification) {
let contentInsets = UIEdgeInsetsZero
self.scrollView.contentInset = contentInsets
self.scrollView.scrollIndicatorInsets = contentInsets
}
Issue
I want just one simple modification - a little more space between the keyboard and an edited field. Because out of the box it looks like this:
I modified CGRect in scrollRectToVisible call, but it changed nothing. What's more, even commenting out the line with scrollRectToVisible had no effect at all - everything worked exactly as before (including scrolling up the content). Checked on iOS 9.2
Replicating, if needed, is trivial - just download the working code using the GitHub link above and comment out the scrollRectToVisible line.
Tested workarounds
Workarounds I tried, but didn't like the final effect:
Increasing contentInset - user could scroll up more then the contentSize
Replacing UIKeyboardDidShowNotification with UIKeyboardWillShowNotification, add another UIKeyboardDidShowNotification observer with just scrollRectToVisible inside - it works, but then there are two scroll up animations, which doesn't look good.
Questions
Why changing contentInset (without scrollRectToVisible call) scrolls content in the scrollView? I've not see in any docs information about such behavior
And more important - what to do, to scroll it up a little more, to have some space between an edited text field and the keyboard?
What did I miss? Is there some easy way to fix it? Or maybe it's better to play with scrollView.contentSize, instead of contentInset?
I haven't found why scrollRectToVisible doesn't work in the scenario described above, however I found other solution which works. In the mentioned Apple article Managing the Keyboard at the very bottom there is a hint
There are other ways you can scroll the edited area in a scroll view
above an obscuring keyboard. Instead of altering the bottom content
inset of the scroll view, you can extend the height of the content
view by the height of the keyboard and then scroll the edited text
object into view.
Solution below is based exactly on extending the height of the content view (scrollView.contentSize). It takes into account orientation change and scrolling back when keyboard is being hidden. Works as required - has some space between the active field and the keyboard, see the image below
Working code
I've placed the full working code on the GitHub: ScrollViewOnKeyboardShow
var animateContenetView = true
var originalContentOffset: CGPoint?
var isKeyboardVisible = false
let offset : CGFloat = 18
override func viewDidLoad() {
super.viewDidLoad()
for case let textField as UITextField in contentView.subviews {
textField.delegate = self
}
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: #selector(ViewController.keyboardWillBeShown(_:)), name: UIKeyboardWillShowNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(ViewController.keyboardWillBeHidden(_:)), name: UIKeyboardWillHideNotification, object: nil)
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldDidEndEditing(textField: UITextField) {
self.activeField = nil
}
func textFieldDidBeginEditing(textField: UITextField) {
self.activeField = textField
}
func keyboardWillBeShown(notification: NSNotification) {
originalContentOffset = scrollView.contentOffset
if let activeField = self.activeField, keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
var visibleRect = self.scrollView.bounds
visibleRect.size.height -= keyboardSize.size.height
//that's to avoid enlarging contentSize multiple times in case of many UITextFields,
//when user changes an edited text field
if isKeyboardVisible == false {
scrollView.contentSize.height += keyboardSize.height
}
//scroll only if the keyboard would cover a bottom edge of an
//active field (including the given offset)
let activeFieldBottomY = activeField.frame.origin.y + activeField.frame.size.height + offset
let activeFieldBottomPoint = CGPoint(x: activeField.frame.origin.x, y: activeFieldBottomY)
if (!CGRectContainsPoint(visibleRect, activeFieldBottomPoint)) {
var scrollToPointY = activeFieldBottomY - (self.scrollView.bounds.height - keyboardSize.size.height)
scrollToPointY = min(scrollToPointY, scrollView.contentSize.height - scrollView.frame.size.height)
scrollView.setContentOffset(CGPoint(x: 0, y: scrollToPointY), animated: animateContenetView)
}
}
isKeyboardVisible = true
}
func keyboardWillBeHidden(notification: NSNotification) {
scrollView.contentSize.height = contentView.frame.size.height
if var contentOffset = originalContentOffset {
contentOffset.y = min(contentOffset.y, scrollView.contentSize.height - scrollView.frame.size.height)
scrollView.setContentOffset(contentOffset, animated: animateContenetView)
}
isKeyboardVisible = false
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animateAlongsideTransition(nil) { (_) -> Void in
self.scrollView.contentSize.height = self.contentView.frame.height
}
}
deinit {
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
notificationCenter.removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}

scrollView in NavController will not reset after displaying keyboard (Swift)

I have tried to follow other guides on here but to no avail.
My app structure currently is a navigation controller that leads to viewcontrollers with scrollview embedded in them. The textfields, textview, buttons etc are on top of the scrollview. When the keyboard appears, and I dismiss it by tapping outside, the scrollview moves up with the keyboard, but does not come down. It seems like this problem is brought on by having the Navigation controller or the nav bar. How can I fix it?
EDIT: I just realized that every time I simulate that particular viewcontroller, everything in it is moved down the same distance before I tap on any textField/textView. Then when the keyboard is shown, it simply moves it up so it looks like how it's setup in storyboard. How do I get rid of the initial downward displacement?
#IBOutlet var scrollView: UIScrollView!
var activeTextView:UIView? = UIView()
#IBOutlet var main: UITextView!
#IBOutlet var initials: UITextField!
#IBOutlet var maleWord: UITextField!
#IBOutlet var maleButton: UIButton!
#IBOutlet var femaleWord: UITextField!
#IBOutlet var femaleButton: UIButton!
#IBOutlet var age: UITextField!
func registerForKeyboardNotifications() {
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self,
selector: "keyboardWillBeShown:",
name: UIKeyboardWillShowNotification,
object: nil)
notificationCenter.addObserver(self,
selector: "keyboardWillBeHidden:",
name: UIKeyboardWillHideNotification,
object: nil)
}
func tapped() {
initials.resignFirstResponder()
main.resignFirstResponder()
age.resignFirstResponder()
}
// Called when the UIKeyboardDidShowNotification is sent.
func keyboardWillBeShown(sender: NSNotification) {
let info: NSDictionary = sender.userInfo!
let value: NSValue = info.valueForKey(UIKeyboardFrameEndUserInfoKey) as! NSValue
let keyboardSize: CGSize = value.CGRectValue().size
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
// If active text field is hidden by keyboard, scroll it so it's visible
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardSize.height
let activeTextViewRect: CGRect? = self.main.frame
let activeTextViewOrigin: CGPoint? = activeTextViewRect?.origin
if (!CGRectContainsPoint(aRect, activeTextViewOrigin!)) {
scrollView.scrollRectToVisible(activeTextViewRect!, animated:true)
}
}
// Called when the UIKeyboardWillHideNotification is sent
func keyboardWillBeHidden(sender: NSNotification) {
self.scrollView .setContentOffset(CGPointMake(0, 0), animated: true)
self .viewDidLayoutSubviews()
self.activeTextView = nil
}
func textViewDidBeginEditing(textView: UITextView) {
self.activeTextView = textView
scrollView.scrollEnabled = true
}
func textViewDidEndEditing(textView: UITextView) {
self.activeTextView = nil
scrollView.scrollEnabled = false
self.scrollView .setContentOffset(CGPointMake(0, 0), animated: true)
}
func textFieldShouldReturn(textField: UITextField) -> Bool // called when 'return' key pressed. return NO to ignore.
{
textField.resignFirstResponder()
return true;
}
override func viewDidLoad() {
super.viewDidLoad()
var tap = UITapGestureRecognizer (target: self, action: ("tapped"))
self.view.addGestureRecognizer(tap)
self.main.delegate = self
self.initials.delegate = self
self.age.delegate = self
self.registerForKeyboardNotifications()
}
Update your function like following
func keyboardWillBeHidden(sender: NSNotification) {
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0)
scrollView.contentInset = contentInsets
self.scrollView .setContentOffset(CGPointMake(0, 0), animated: true)
self .viewDidLayoutSubviews()
self.activeTextView = nil
}

Resources