So I have the following code:
//MARK: - Textfield delegate Methods
func textFieldDidBeginEditing(textField: UITextField)
{
// update view based on updates in the constraints
self.view.layoutIfNeeded()
//perform animation to grow the dockview
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.dockViewHeightConstraint.constant = self.keyBoardHeight + 60
self.view.layoutIfNeeded()
}, completion: nil)
}
and
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
keyBoardHeight = contentInsets.bottom
}
}
What I am trying to do is the following. When the user clicks on the textfield, the keyboard should open up, and the View which I made should move up.
I get the height from the keyboard from the keyboardWillShow function that is triggered by a NSNotification.
The problem is the following: when I run the code, the textFieldDidBeginEditing is being called before the NSNotification, making my self.keyBoardHeight value 0 (init value). Is there a way I can call the NSNotification part before pressing on the textField?
Set up a isKeyboardShown flag in your viewController to keep track whether keyboard is shown or not, the height which intersects with your element "myElement" can be captured in fieldMovement property as below
if !isKeyboardShown
{
if let info = notification.userInfo
{
if fieldMovement == nil
{
let window = UIApplication.sharedApplication().keyWindow
var keyboardEndRect = CGRectZero
//Get frame of keyboard view object
((info[UIKeyboardFrameEndUserInfoKey] as! NSValue)).getValue(&keyboardEndRect)
/* Convert the frame from the window's coordinate system to our view's coordinate system */
keyboardEndRect = view.convertRect(keyboardEndRect, fromView: window)
/* Find out how much of our view is being covered by the keyboard */
fieldMovement = CGRectIntersection(myElement.frame, keyboardEndRect).height
}
//Get animation duration of keyboard
var animationDuration: NSTimeInterval = 0
(info[UIKeyboardAnimationDurationUserInfoKey] as! NSValue).getValue(&animationDuration)
// Move views up by intersected height
UIView.animateWithDuration(animationDuration)
{
//animate your objects here
}
}
isKeyboardShown = true
}
Related
I have a text input field at the bottom of my view, which I'm trying to animate up and down to stay on top of the keyboard.
func setupKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrame), name: UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func handleKeyboardWillChangeFrame(notification: NSNotification) {
let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let keyboardDuration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double)
print(keyboardFrame)
orderDetailView?.textInputViewBottomAnchor?.constant = -keyboardFrame!.height
UIView.animate(withDuration: keyboardDuration!) {
self.view.layoutIfNeeded()
}
}
OrderDetailView is the view for the viewcontroller.
The textinputview is the part that animates, and it works correctly when the keyboard first shows up, but does not animate back when I send a message and the keyboard resigns first responder, nor if I resignfirstresponder by clicking outside the keyboard.
When I print the cgrect value from keyboardFrameEndUserInfoKey, it returns the same frame value as when the keyboard is present (instead of 0).
This only seems to work properly when I drag down the keyboard from the view.
Thanks for your help.
In your case the height is still non-zero when keyboard hides which I assume is your issue. You need to convert keyboard frame to your view coordinate system and setup constraints according to that. Check the following:
#objc private func onKeyboardChange(notification: NSNotification) {
guard let info = notification.userInfo else { return }
guard let value: NSValue = info[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
let newFrame = value.cgRectValue
if let durationNumber = info[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber, let keyboardCurveNumber = info[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
let duration = durationNumber.doubleValue
let keyboardCurve = keyboardCurveNumber.uintValue
UIView.animate(withDuration: duration, delay: 0, options: UIViewAnimationOptions(rawValue: keyboardCurve), animations: {
self.updateFromKeyboardChangeToFrame(newFrame)
}, completion: { _ in
// After animation
})
} else {
self.updateFromKeyboardChangeToFrame(newFrame)
}
}
private func updateFromKeyboardChangeToFrame(_ keyboardFrame: CGRect) {
let view: UIView! // Whatever view that uses bottom constraint
let bottomConstraint: NSLayoutConstraint! // Your bottom constraint
let constant = view.bounds.height-max(0, view.convert(keyboardFrame, from: nil).origin.y)
bottomConstraint.constant = max(0, constant)
view.layoutIfNeeded()
}
In your case you seem to use
let view = self.view
let bottomConstraint = orderDetailView?.textInputViewBottomAnchor
and it depends on how you define your constraint but it seems you will need to use negative values as bottomConstraint.constant = -max(0, constant).
I have a pageviewcontroller, which scrolls horizontally, displaying questions and answers.
The contentView for the pageviewcontroller is populated with a scrollview, and inside of this is some stack views, one of which has a textView, for user entry.
When the user taps on the textView, the keyboard pops up, sometimes covering up the textView. When I try to scroll vertically, as I would expect scrollview to allow, the scrollview doesn't respond, I can only scroll horizontally to the next page view.
The problem is that when the keyboard pops up after the user taps in the textview, the textview may be hidden underneath the keyboard, so the user can't see what he's typing. I'd like to have the view scroll upward when he keyboard is tapped, so the user can see what is being typed.
That's called "Keyboard managing"
You have to:
1) Set observer for keyboard appear and keyboard disappear action
2) Update bottom constraint, when keyboard appears.
3) Update it again, when keyboard disappears.
4) Delete observers, when view will disappear.
Step by step instructions, for example here: Move view with keyboard using Swift
But my recommendation is to update constraints, not origins, as in instruction. Or, maybe, you can set an offset for content
you should set an bottom constraint into your stack view
and then controll drag that in your class like this :
class yourClassViewController: UIViewController {
// MARK: Properties
#IBOutlet weak var bottomConstraint: NSLayoutConstraint!
then in your viewDidLoad method write like this:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(self.keyboardNotification(notification:)),
name: NSNotification.Name.UIKeyboardWillChangeFrame,
object: nil)
// Do any additional setup after loading the view.
}
and below that:
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc func keyboardNotification(notification: NSNotification) {
if let userInfo = notification.userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let endFrameY = endFrame?.origin.y ?? 0
let duration:TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseInOut.rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if endFrameY >= UIScreen.main.bounds.size.height {
self.bottomConstraint?.constant = 0.0
} else {
self.bottomConstraint?.constant = endFrame?.size.height ?? 0.0
}
UIView.animate(withDuration: duration,
delay: TimeInterval(0),
options: animationCurve,
animations: { self.view.layoutIfNeeded() },
completion: nil)
}
}
for disappear keyboard in touchsBegan method you should write like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
Use notification observers to handle keyboard show and hide events:
// keyboard will show
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowAction(_:)), name: .UIKeyboardWillShow, object: nil)
// keyboard will hide
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideAction(_:)), name: .UIKeyboardWillHide, object: nil)
You can do something like this when the keyboard is about to appear:
// keyboard will show action
#objc private func keyboardWillShowAction(_ notification: NSNotification) {
currentScrollViewOffset = scrollView.contentOffset
let keyboardFrame = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue
let keyboardHeight = keyboardFrame?.cgRectValue.height
let padding: CGFloat = 32
// if text field view would be obstructed by keyboard, apply minimum-needed scroll view offset
if activeTextFieldMaxYOnScreen! > (UIScreen.main.bounds.height - keyboardHeight! - padding) {
let temporaryOffset = activeTextFieldMaxYOnScreen! - (UIScreen.main.bounds.height - (keyboardHeight! + padding))
scrollView.setContentOffset(CGPoint(x: 0, y: currentScrollViewOffset.y + temporaryOffset), animated: true)
}
scrollView.addGestureRecognizer(tapToDismissKeyboard)
scrollView.isScrollEnabled = false
}
And then revert when the keyboard is dismissed:
// keyboard will hide action
#objc private func keyboardWillHideAction(_ notification: NSNotification) {
scrollView.setContentOffset(currentScrollViewOffset, animated: true)
scrollView.removeGestureRecognizer(tapToDismissKeyboard)
scrollView.isScrollEnabled = true
}
It's also good practice to remove observers in the deinit method:
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil)
Note that currentScrollViewOffset and activeTextFieldMaxYOnScreen are instance properties of the view controller. To get the activeTextFieldMaxYOnScreen value, you can get it from the text field delegate:
// text field should begin editing
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
// convert text field line's max-y to perceived max-y on the screen
let line = textField.viewWithTag(123)
activeTextFieldMaxYOnScreen = (line?.convert((line?.bounds.origin)!, to: nil).y)! + (line?.frame.height)!
return true
}
I have searched
here: Move a view up only when the keyboard covers an input field
here: Move textfield when keyboard appears swift
here: How to make a UITextField move up when keyboard is present?
and here: https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html
Unfortunately, all of the links and seemingly everywhere else I can find do not give a good clean solution that I am looking for. They are either outdated with Obj-c code, or plain do not work in the current iteration of Xcode 9 with Swift 4.
How do I manage a keyboard that is covering text fields at the bottom of my view? I want the screen's view to move only when they keyboard is covering the text field view, without using a scroll view, with the ability to animate this to make it look pretty rather than have it just snap, and most importantly I do not want to use an outside library.
CoreAnimation libraries from Apple are great and all, but all of the sample code is outdated and in objective c which is deprecated at this point (I cannot believe that Apple isn't updating their documentation).
If someone could point me in the right direction to updated and current code or a library from Apple I am missing that will specifically address this issue, it would be much appreciated.
You can use IQKeyboardManagerSwift to solve your issue easily and fast.
Use below pod in your pod file Which give support to Swift 4.
pod 'IQKeyboardManagerSwift', '5.0.0'
Here is link to implement IQKeyboardManagerSwift.
https://github.com/hackiftekhar/IQKeyboardManager
Thanks!!!
This code will work, making your textField animating to above keyboard if its frame intersects with that of keyboard and animating back to original position on keyboard hide.
#IBOutlet weak var textField: UITextField!
var offsetY:CGFloat = 0
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardFrameChangeNotification(notification:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
func keyboardFrameChangeNotification(notification: Notification) {
if let userInfo = notification.userInfo {
let endFrame = userInfo[UIKeyboardFrameEndUserInfoKey] as? CGRect
let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double ?? 0
let animationCurveRawValue = (userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIViewAnimationOptions.curveEaseInOut.rawValue)
let animationCurve = UIViewAnimationOptions(rawValue: UInt(animationCurveRawValue))
if let _ = endFrame, endFrame!.intersects(self.textField.frame) {
self.offsetY = self.textField.frame.maxY - endFrame!.minY
UIView.animate(withDuration: animationDuration, delay: TimeInterval(0), options: animationCurve, animations: {
self.textField.frame.origin.y = self.textField.frame.origin.y - self.offsetY
}, completion: nil)
} else {
if self.offsetY != 0 {
UIView.animate(withDuration: animationDuration, delay: TimeInterval(0), options: animationCurve, animations: {
self.textField.frame.origin.y = self.textField.frame.origin.y + self.offsetY
self.offsetY = 0
}, completion: nil)
}
}
}
}
This piece of code worked for me.
In case of multiple textfields
I have implemented only for the textfields which are at the bottom (without using notification observer).
If you are using scrollView, this code might be helpful
(scrollViewDidScroll is optional)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.contentSize = CGSize(width: self.scrollView.frame.size.width, height: (scrollView.frame.size.height + 300))// To be more specific, I have used multiple textfields so wanted to scroll to the end.So have given the constant 300.
}
func textFieldDidBeginEditing(_ textField:UITextField) {
self.scrollView.setContentOffset(textField.frame.origin, animated: true)
}
and if you want to set the textfields position according to the view,
try this:
func textFieldDidBeginEditing(_ textField:UITextField){
textField.frame.origin.y = textField.frame.origin.y - 150 //(If have not used contentsizing the scroll view then exclude this line)default origin takes the texfield to the top of the view.So to set lower textfields to proper position have used the constant 150.
self.scrollView.setContentOffset(textField.frame.origin, animated: true)
}
To do specifically for textfields at the bottom. Check their tag value textfield.tag in textFieldDidBeginEditing
func textFieldDidBeginEditing(_ textField:UITextField){
if textField.tag = 4 { //tag value of the textfield which are at the bottom
self.scrollView.setContentOffset(textField.frame.origin, animated: true)
}
}
If you implemented textfields in tableView go with notification observer which is explained below.
If there are multiple textfields in a tableView preferably go with Notification Observer
override func viewDidAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardDidHide, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardHeight = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height {
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0)
}
}
#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 = UIEdgeInsetsMake(0, 0, 0, 0)
})
}
deinit {
print("denit")
NotificationCenter.default.removeObserver(self)
}
worked perfectly for me:
http://www.seemuapps.com/move-uitextfield-when-keyboard-is-presented
If delegates are set right,
func textFieldDidBeginEditing(_ textField: UITextField) {
moveTextField(textField, moveDistance: -250, up: true)
}
// Finish Editing The Text Field
func textFieldDidEndEditing(_ textField: UITextField) {
moveTextField(textField, moveDistance: -250, up: false)
}
// Hide the keyboard when the return key pressed
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
// Move the text field in a pretty animation!
func moveTextField(_ textField: UITextField, moveDistance: Int, up: Bool) {
let moveDuration = 0.3
let movement: CGFloat = CGFloat(up ? moveDistance : -moveDistance)
UIView.beginAnimations("animateTextField", context: nil)
UIView.setAnimationBeginsFromCurrentState(true)
UIView.setAnimationDuration(moveDuration)
self.view.frame = self.view.frame.offsetBy(dx: 0, dy: movement)
UIView.commitAnimations()
}
Add an extensio to Uiview:
import UIKit
//Binding view to keyboard changes
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()
})
}
}
I'm working on an iOS app and currently all my elements are in a scroll view and when the keyboard is present I move the view up 250 pts. This solved my problem but the keyboard is always a different size per device.
How could I detect how far from the bottom of the screen my text field is and how tall the keyboard is?
You should observe the notification for showing and hiding the keyboard. And after that you can get the exact keyboard size and either shift or change the content insets of your scroll view. Here's a sample code:
extension UIViewController {
func registerForKeyboardDidShowNotification(scrollView: UIScrollView, usingBlock block: (NSNotification -> Void)? = nil) {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidShowNotification, object: nil, queue: nil, usingBlock: { (notification) -> Void in
let userInfo = notification.userInfo!
let keyboardSize = userInfo[UIKeyboardFrameBeginUserInfoKey]?.CGRectValue.size
let contentInsets = UIEdgeInsetsMake(scrollView.contentInset.top, scrollView.contentInset.left, keyboardSize!.height, scrollView.contentInset.right)
scrollView.scrollEnabled = true
scrollView.setContentInsetAndScrollIndicatorInsets(contentInsets)
block?(notification)
})
}
func registerForKeyboardWillHideNotification(scrollView: UIScrollView, usingBlock block: (NSNotification -> Void)? = nil) {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardWillHideNotification, object: nil, queue: nil, usingBlock: { (notification) -> Void in
let contentInsets = UIEdgeInsetsMake(scrollView.contentInset.top, scrollView.contentInset.left, 0, scrollView.contentInset.right)
scrollView.setContentInsetAndScrollIndicatorInsets(contentInsets)
scrollView.scrollEnabled = false
block?(notification)
})
}
}
extension UIScrollView {
func setContentInsetAndScrollIndicatorInsets(edgeInsets: UIEdgeInsets) {
self.contentInset = edgeInsets
self.scrollIndicatorInsets = edgeInsets
}
}
And in your UIViewController in which you want to shift the scrollview, just add next lines under the viewDidLoad() function
override func viewDidLoad() {
super.viewDidLoad()
registerForKeyboardDidShowNotification(scrollView)
registerForKeyboardWillHideNotification(scrollView)
}
I'm currently work on this and found a solution. First you need to add a notification to the view controller to identify whether the keyboard is on or not. For that you need to register this notification in viewDidLoad().
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardWillChangeFrameNotification, object: nil)
}
And also don't forget to remove this notification, when the view controller removed from the view. So remove this notification on viewDidDisappear().
override func viewDidDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillChangeFrameNotification, object: nil)
}
And the final thing is to manage the scroll view when the keyboard is on or off. So first you need to identify the keyboard height.Then for pretty smooth animation, you can use keyboard animation mood and duration time to animate your scroll view.
func keyboardDidShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue()
let duration:NSTimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.unsignedLongValue ?? UIViewAnimationOptions.CurveEaseInOut.rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if endFrame?.origin.y >= UIScreen.mainScreen().bounds.size.height {
//isKeyboardActive = false
UIView.animateWithDuration(duration,
delay: NSTimeInterval(0),
options: animationCurve,
animations: {
// move scroll view height to 0.0
},
completion: { _ in
})
} else {
//isKeyboardActive = true
UIView.animateWithDuration(duration,
delay: NSTimeInterval(0),
options: animationCurve,
animations: {
// move scroll view height to endFrame?.size.height ?? 0.0
},
completion: { _ in
})
}
}
}
#noir_eagle answer seems right.
But there may be is a simpler solution. Maybe you could try using IQKeyboardManager. It allows you to handle these keyboard things in a simple and seamless way.
I think you really should, at least, spend few minutes looking at it.
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)
}