Search code examples
iosswiftkeyboarduikit

How to calculate proper keyboard contentInset for UIScrollView inside of a modally presented form sheet UIViewController


I am having an issue in which relying on convertRect to properly report a y position to use to calculate a contentInset is not working on iOS 12. This approach used to work on earlier iOS versions:

@objc func keyboardVisibilityChanged(notification: Notification) {
    guard let userInfo = notification.userInfo else {
        assertionFailure()
        return
    }

    let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    let keyboardViewEndFrame = scrollView.convert(keyboardScreenEndFrame, from: view.window!)

    if notification.name == UIResponder.keyboardWillHideNotification {
        scrollView.contentInset = .zero
        scrollView.scrollIndicatorInsets = .zero
    } else {
        let insets = UIEdgeInsets(top: 0, left: 0, bottom: (keyboardViewEndFrame.origin.y - keyboardViewEndFrame.size.height) , right: 0)
        scrollView.contentInset = insets
        scrollView.scrollIndicatorInsets = insets
    }
}

However, this code, while achieving extremely close visual results, is not exact and also breaks on iPhone, where the modal is presented fullscreen.

Broken Inset


Solution

  • Apple states in their documentation:

    Note: The rectangle contained in the UIKeyboardFrameBeginUserInfoKey and UIKeyboardFrameEndUserInfoKey properties of the userInfo dictionary should be used only for the size information it contains. Do not use the origin of the rectangle (which is always {0.0, 0.0}) in rectangle-intersection operations. Because the keyboard is animated into position, the actual bounding rectangle of the keyboard changes over time.

    So I came up with the following solutions that seems to work well on iOS 13, 12 and 11, including safe areas, modal form sheets, and hardware keyboards):

    // MARK: - Keyboard Notifications
    @objc func keyboardVisibilityChanged(notification: Notification) {       
        if notification.name == UIResponder.keyboardWillHideNotification {
            scrollView.contentInset = .zero
            scrollView.scrollIndicatorInsets = .zero
        } else {
            guard let userInfo = notification.userInfo,
                let value = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
                let window = view.window else {
                    assertionFailure()
                    return
            }
    
            let keyboardEndFrameInWindowCoordinates = value.cgRectValue
            let viewFrameInWindowCoordinates = window.convert(scrollView.frame,
                                                              from: scrollView.superview)
    
            let contentInsetBottom: CGFloat
    
            // If the keyboard is below us, no need to do anything.
            // This can happen when a hardware keyboard is attached to a modal form sheet on iPad
            if keyboardEndFrameInWindowCoordinates.origin.y >= viewFrameInWindowCoordinates.maxY {
                contentInsetBottom = 0
            } else {
                let bottomEdgeOfViewInWindowBottomCoordinates = window.frame.maxY - viewFrameInWindowCoordinates.maxY
                contentInsetBottom = keyboardEndFrameInWindowCoordinates.height - bottomEdgeOfViewInWindowBottomCoordinates - view.safeAreaInsets.bottom
            }
    
            let insets = UIEdgeInsets(top: 0,
                                 left: 0,
                                 bottom: contentInsetBottom,
                                 right: 0)
    
            scrollView.scrollIndicatorInsets = insets
        }
    }