I have created a form with 4 fields and one button. The view hierarchy looks like this: Main UIVIew, View (renamed contentView), on top of contentView I have 4 fields and 1
I think you should only move the fields up if they are covered by the keyboard, something like what i cooked up a couple of days ago:
let keyboardScreenEndFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue ?? NSValue()).cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
UIView.animate(withDuration: 0.2) {
if notification.name == Notification.Name.UIKeyboardWillHide {
self.view.frame = CGRect(x: 0, y: 0, width: self.view.width, height: self.view.height)
} else {
let offset = (self.view.frame.size.height - self.activeField.frame.maxY) - keyboardViewEndFrame.height
if offset < 0 {
self.view.frame = CGRect(x: 0, y: offset, width: self.view.width, height: self.view.height)
} else {
self.view.frame = CGRect(x: 0, y: 0, width: self.view.width, height: self.view.height)
}
}
}
Basically you just need to add logic for keyboard handling timing, and you should handle it if keyboard frame goes over textfield frame. Hope it helps.
So I think you are close and I am not sure if it is a code issue or an autolayout issue. My guess is you are getting complaints about your scrollview not know the content size so I will cover both.
EDIT: Also you button will have to be below the view container that I first add and will need to be handled separately from the scrollview. Do not put it in the scrollview.
The methods work below except add 50 or whatever to the layout bottom for the view that will hold the scrollview. Also the method below will help make edits
Autolayout: First for forms that will only take up on page I like to start by adding a view to the storyboard and pin in top layout guide, (whatever space you need for button), left and right. Then I add my ScrollView(pin the scrollview to that view) to the view just added.Then next I add my content view to the scrollview. Now I pin this to the scrollview. You will see that autolayout is still not happy. So why the first view and how to fix this. I drag from the contentView to the view holding the scrollview and choose equal heights and equal widths. Now you will not have auto layout screaming at you. Note:This works for content you want to fill the first view size but allow it to scroll to avoid the keyboard. See images
After adding this equal heights I can continue with the storyboard. I set the textfields up. The bottom textfield you may or may not want to pin it to the bottom but if you do make it >= yourNumber.
EDIT: Now add your NEXT Button to the storyboard below the view that is holding everything. The button has to be pinned to the bottom of the main view with a 0 to stick to the keyboard. It will now look like this.
Obviously this conflicts slightly with the initial images but all you have to do is increase the space to the bottom layout guide just make sure your button is added to the main view and not the view holding the scrollview. Now connect your button to the controller in an iboutlet. we will need it.
Next make sure you have the right keyboard in your simulator. **Not using hardware keyboard
Finally the code. some of it you would need to substitute your textfield variables as I looped through the subviews to set the delegate. I also added padding to the scroll up. You should move your deRegister to deint(). See my code and finally you might want to scroll the scroll view on keyboard will appear instead of did appear but I did not alter this.
import UIKit
class ViewController: UIViewController,UITextFieldDelegate {
//added in storyboard. removed the code
@IBOutlet weak var nextButton: UIButton!
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
//called whenever keyboard is shown/hidden
registerForKeyboardNotifications()
//when identifies single or multiple taps, call DismissKeyboard
var tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "DismissKeyboard")
contentView.addGestureRecognizer(tap)
//disable scroll bouncing
scrollView.bounces = false
//replace with your textfields
for subs in self.contentView.subviews{
if subs is UITextField{
print("setting")
(subs as! UITextField).delegate = self
}
}
}
//Call this function when the tap is recognized.
func DismissKeyboard(){
contentView.endEditing(true)
}
// Stop Editing on Return Key Tap.
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
//edited for next button
weak var activeField: UITextField?
func keyboardDidShow(_ notification: Notification) {
//when a textfield is edited lift the button above the keyboard
if let activeField = self.activeField,let keyboardSize =
(notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as?
NSValue)?.cgRectValue {
//20 in insets and offset is just padding
let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom:
keyboardSize.height + 20 + nextButton.bounds.height, right: 0.0)
self.scrollView.contentInset = contentInsets
var aRect = self.view.frame
aRect.size.height -= keyboardSize.height
let bottomPoint = CGPoint(x: activeField.frame.origin.x, y:activeField.frame.origin.y)
if aRect.contains(bottomPoint){
let scrollPoint = CGPoint(x: 0.0, y: bottomPoint.y - keyboardSize.height - 20 - nextButton.bounds.height)
scrollView.setContentOffset(scrollPoint, animated: true)
}
}
}
func keyboardWillHide(_ notification: Notification) {
let contentInsets = UIEdgeInsets.zero
self.scrollView.contentInset = contentInsets
self.scrollView.scrollIndicatorInsets = contentInsets
}
//Keep track of which textfield is being edited to make sure the field is visible when keyboard pops up
func textFieldDidBeginEditing(_ textField: UITextField) {
self.activeField = textField
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextFieldDidEndEditingReason) {
self.activeField = nil
}
//register for keyboard notifications
func registerForKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector:
#selector(keyboardDidShow),
name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector:
#selector(keyboardWillHide), name:
NSNotification.Name.UIKeyboardWillHide, object: nil)
}
//remove keyBoard observers
func deregisterFromKeyboardNotifications() {
NotificationCenter.default.removeObserver(self, name:
NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.removeObserver(self, name:
NSNotification.Name.UIKeyboardWillHide, object: nil)
}
deinit {
//deregister keyboard notifications
deregisterFromKeyboardNotifications()
}
} //end of class
Now one more step. We have to handle the move up of the button. Instead of just killing this controller and putting more handling in it you can subclass the bottomconstraint to handle it. (Just make sure not to add a top constraint to the bottom.) Here is a constraint to drop in your project.
import UIKit
class AvoidingConstraint: NSLayoutConstraint {
private var offset : CGFloat = 0
private var keyboardVisibleHeight : CGFloat = 0
override public func awakeFromNib() {
super.awakeFromNib()
offset = constant
NotificationCenter.default.addObserver(self, selector: #selector(AvoidingConstraint.keyboardWillShowNotification(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(AvoidingConstraint.keyboardWillHideNotification(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Notification
func keyboardWillShowNotification(_ notification: Notification) {
if let userInfo = notification.userInfo {
if let frameValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let frame = frameValue.cgRectValue
keyboardVisibleHeight = frame.size.height
}
self.updateConstant()
switch (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber, userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber) {
case let (.some(duration), .some(curve)):
let options = UIViewAnimationOptions(rawValue: curve.uintValue)
UIView.animate(
withDuration: TimeInterval(duration.doubleValue),
delay: 0,
options: options,
animations: {
UIApplication.shared.keyWindow?.layoutIfNeeded()
return
}, completion: { finished in
})
default:
break
}
}
}
func keyboardWillHideNotification(_ notification: NSNotification) {
keyboardVisibleHeight = 0
self.updateConstant()
if let userInfo = notification.userInfo {
switch (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber, userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber) {
case let (.some(duration), .some(curve)):
let options = UIViewAnimationOptions(rawValue: curve.uintValue)
UIView.animate(
withDuration: TimeInterval(duration.doubleValue),
delay: 0,
options: options,
animations: {
UIApplication.shared.keyWindow?.layoutIfNeeded()
return
}, completion: { finished in
})
default:
break
}
}
}
func updateConstant() {
self.constant = offset + keyboardVisibleHeight
}
}
Add this to a file in your project. Then all you have to do is on the bottom constraint for your button in story board change it to this subclass. Just make sure there is no top constraint. The view holding the scrollview needs to have a bottom constraint to the main view and not the button with enough space for the button. Run the project and enjoy. See link for test project if this explanation is not enough. https://www.dropbox.com/s/ir5x324mvhhne64/ScrollView.zip?dl=0