Search code examples
iosswiftuiscrollviewuikeyboard

Setting UIScrollView's contentInset for keyboardWillShowNotification not working properly


I have a ViewController with the following structure ((x) indicating the level):

UIViewController    (1)
  - NavigationBar   (2)
  - UIScrollView    (2)
    - UIView        (3)
      - UITextField (4)
      - UITextField (4)
      - UITextField (4)
      - UITextField (4)
      - UIButton    (4)
  • All elements of level 4 are vertically constrained to eachother with a spacing of 16.
  • The first and last elements of level 4 are constrained to the UIView's (3) top and bottom.
  • The UIView (3) is constrained with top and bottom to the UIScrollView (2).
  • The UIScrollView (2) is constrained to the NavigationBar's bottom (2) and the superview's bottom (1)
  • (Of course there are the necessary horizontal constraints as well!)

The UIView (3) has the following constraints:

  • Leading constraint of 0 to all subViews.
  • Trailing constraint of 0 to all subviews.
  • Bottom space of 24 to UIButton (should add some extra spacing)
  • Top space of 24 to the topmost UITextField (top spacing)
  • Top space of 0 to superView (UIScrollView)
  • Bottom space of 0 to superView (UIScrollView)
  • 'Equal width - minus 32' to NavigationBar (so — fixed width)

In the viewDidLoad of the viewController, I call this:

registerForKeyboardWillShowNotification(self.scrollView)
registerForKeyboardWillHideNotification(self.scrollView)

Where registerForKeyboard...ShowNotification is an extension of UIViewController:

extension UIViewController
{
    /// Act when keyboard is shown, by adding contentInsets to the scrollView.
    func registerForKeyboardWillShowNotification(_ scrollView: UIScrollView, usingBlock block: ((CGSize?) -> Void)? = nil)
    {
        _ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification,
                                                   object: nil, queue: nil)
        { notification in
            let userInfo      = notification.userInfo!
            let keyboardSize  = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue.size
            let contentInsets = UIEdgeInsets(top: scrollView.contentInset.top,
                                             left: scrollView.contentInset.left,
                                             bottom: keyboardSize.height,
                                             right: scrollView.contentInset.right)

            scrollView.contentInset = contentInsets
            block?(keyboardSize)
        }
    }

    /// Act when keyboard is hidden, by removing contentInsets from the scrollView.
    func registerForKeyboardWillHideNotification(_ scrollView: UIScrollView, usingBlock block: ((CGSize?) -> Void)? = nil)
    {
        _ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification,
                                                   object: nil, queue: nil)
        { notification in
            let userInfo = notification.userInfo!
            let keyboardSize = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue.size
            let contentInsets = UIEdgeInsets(top: scrollView.contentInset.top,
                                             left: scrollView.contentInset.left,
                                             bottom: 0,
                                             right: scrollView.contentInset.right)

            scrollView.contentInset = contentInsets
            block?(keyboardSize)
        }
    }
}

However, when the keyboard shows, it doesn't inset the scrollView (enough). I debugged, and this is the case:

  • Regular keyboard: height = 216
  • Regular keyboard with suggestion bar: height = 260
  • iPhone X keyboard with suggestion bar: height = 291

I first though the suggestion bar could be the problem, but it's not.

In registerForKeyboardWillShowNotification I changed bottom: keyboardSize.height to bottom: keyboardSize.height + 30, which gives exactly the same result (I see the same part of the button that's partially hidden behind the keyboard). Once I add 50 or more, it finally seems to make a small difference.

  • Instead of keyboardWillShowNotification I tried keyboardDidShowNotification, this doesn't make a difference.
  • Instead of keyboardFrameEndUserInfoKey I tried keyboardFrameBeginUserInfoKey, this doesn't make a difference.

What am I missing here?


Solution

  • Unfortunately I haven't been able to solve this, I'm unsure why this doesn't work as expected.

    However, I got the expected behaviour by using a UIStackView inside the UIScrollView. I used this article as reference.


    The UI layout

    • UIViewController (1)
      • NavigationBar (2)
      • UIScrollView (2)
      • UIStackView (3)
        • UITextField (4)
        • UITextField (4)
        • UITextField (4)
        • UITextField (4)
        • UIButton (4)

    The UIScrollView

    • ScrollView's leading and trailing are constrained with 16 to the superView.
    • ScrollView's top is constrained with 0 to the navigationBar's bottom.
    • ScrollView's bottom is constrained with 0 to the superView's bottom.

    The UIStackView

    • StackView's leading and trailing are constrained with 0 to the scrollView.
    • StackView has an equal width to scrollView.
    • StackView's top and bottom are constrained with 24 to the scrollView, to get the desired spacing to the navigationBar, and between the button and the keyboard.
    • StackView is setup as axis=vertical, alignment=fill, distribution=fill, spacing=24.

    The NavigationBar

    The NavigationBar is a custom class that derives its height from its contents. This didn't have a height constraint set, but a placeholder height of 100. The NavigationBar would fill the entire screen. This is solved by removing the placeholder height, and adding any height constraint with a low priority (in this case a priority of 1).


    The initial code for applying the keyboard inset works now.

    /// Act when keyboard is shown, by adding contentInsets to the scrollView.
    func registerForKeyboardWillShowNotification(_ scrollView: UIScrollView, usingBlock block: ((CGSize?) -> Void)? = nil)
    {
        _ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification,
                                                   object: nil, queue: nil)
        { notification in
            let userInfo      = notification.userInfo!
            let keyboardSize  = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue.size
            let contentInsets = UIEdgeInsets(top: scrollView.contentInset.top,
                                             left: scrollView.contentInset.left,
                                             bottom: keyboardSize.height,
                                             right: scrollView.contentInset.right)
    
            scrollView.contentInset = contentInsets
            block?(keyboardSize)
        }
    }