I am very confused about UIScrollview auto layout
I want to make a very standard IOS Application with some textfields and textviews. When the keyboard appears, I want the current responder to be visible on screen.
It seems to me very simple, There ara many posts on the subject, but I don't obtain the correct result, and I think it comes from the layout of my scrollview and it's contentView.
For the layout, I do the following:
The left, top, right and bottom of my UIscrollView is pinned on the view of the UIViewController.
The left, top, right and bottom of the controlView is pinned on ContentLayoutGuide of the UIScrollview.
And, as I want the scroll to be only vertical, I have the width of the Content View to be equal to the width of the FrameLayoutGuide of the UIScrollView.
At this point, I have an error in interface Builder, which is: Scrollview needs constraint for Y position.
After that, in IB, I position constraints of all the fields in the contentView.
I still have the layout error in Interface Builder. Some posts say that the last subview in contentView as to be pinned to the bottom of the contentView. But I think it is not good, because it will give a height for the content view, or this height depends of the device to be used.
So I click on the error in Interface Builder, then on "Add missing constraints", and the new constraint pin the bottom of contentView to bottom of FrameLayoutGuide of the UIScrollView.
No error at this point, but if I run under simulator, I have the warning: "LayoutConstraints] Unable to simultaneously satisfy constraints."
No Matters, I go on
To eventually make the scroll when a UIControl becomes first responder, I write the following in my UIViewController:
`class BaseViewController: UIViewController { // visibleRect is the part of the contentView which is visible on screen var visibleRect: CGRect!
// scrollOrigin is the initial contentOffset of the scrollview
var scrollOrigin: CGPoint!
override func viewDidLoad() {
super.viewDidLoad()
registerKeyboardNotifications()
Let kbdGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard(_:)))
view.addGestureRecognizer(kbdGestureRecognizer)
}
func registerKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
@objc func dismissKeyboard(_ sender: UITapGestureRecognizer) {
view.endEditing(true)
if let scrollview = scrollview, let scrollOrigin = scrollOrigin {
scrollview.setContentOffset(scrollOrigin, animated: true)
}
}
@objc func adjustForKeyboard(notification: Notification) {
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardViewEndFrame = view.convert(keyboardValue.cgRectValue, from: view.window)
if let scrollview = scrollview {
visibleRect = scrollview.subviews[0].frame
} else {
visibleRect = view.frame
}
visibleRect.size.height -= keyboardViewEndFrame.size.height
}`
In adjustForKeyboard function, scrollview.subviews[0].frame as a negative y position.
All UIControls are subclassed so that in becomeFirstResponder, it calls the following function of UIViewController:
public func currentResponderChange(_ newResponder: UIResponder?) {
currentResponder = newResponder
if let scrollview = scrollview, let currentResponder = currentResponder as? UIView, currentResponder.isFirstResponder, let view = controllerView {
if scrollOrigin == nil {
scrollOrigin = scrollview.contentOffset
}
var frameConverted = view.convert(currentResponder.frame, from: view.window)
// si le scroll est uniquement vertical, on met x à zéro
frameConverted.origin.x = 0
if let scrollOffset = scrollOrigin {
frameConverted.origin.y -= scrollOffset.y
}
if visibleRect.contains(frameConverted) {
if let scrollOrigin = scrollOrigin {
scrollview.setContentOffset(scrollOrigin, animated: true)
}
} else {
var delta = (currentResponder.frame.origin.y+currentResponder.frame.size.height) - visibleRect.size.height //- scrollview.contentOffset.y
if let scrollOrigin = scrollOrigin {
delta -= scrollOrigin.y
}
scrollview.setContentOffset(CGPoint(x: 0, y: delta), animated: true)
}
}
}
It don't work, because of the visibleRect, which is not correct. And I think it comes from initial layout in Interface Builder. the view is effectively scrolled, but not for the correct amount
Can someone helps?
I usually don’t use scrollView to do this move when keyboard shows.
I simply put all items (textFields…) in a UIView.
On tap in a field, I compute how much space is available below.
If enough for keyboard, do nothing.
If not, move the whole view up as needed for extra space.
If you want I can send you an example.