iPhone X keyboard appear showing extra space
Asked Answered
N

6

27

I have created a chat UI in which I have added a constraint for the tableView to the bottom of the screen. I am changing the constraint value by adding the height of the keyboard which is working fine in all the devices except iPhone X.

UI when key board is not visible:

enter image description here

Which is fine.

Problem is when keyboard appears blank space is visible in between the textView and the keyboard:

enter image description here

Do I have to try for a different approach for this or it can be resolved using constraints ?

Nanete answered 14/11, 2017 at 12:43 Comment(0)
G
50

Try subtracting the height of the safe area's bottom inset when calculating the value for your constraint.

Here is a sample implementation which handles a UIKeyboardWillChangeFrame notification.

@objc private func keyboardWillChange(_ notification: Notification) {
    guard let userInfo = (notification as Notification).userInfo, let value = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
    let newHeight: CGFloat
    if #available(iOS 11.0, *) {
        newHeight = value.cgRectValue.height - view.safeAreaInsets.bottom
    } else {
        newHeight = value.cgRectValue.height
    }
    myConstraint.value = newHeight
}
Gabriello answered 14/11, 2017 at 21:55 Comment(6)
Thanks bro. I was going to implement this logic only but you answered properly. Isn't it a bit weird that we have to check for the OS ? UIKeyboardFrameEndUserInfoKey should return height deducting the safe area insets.Nanete
@Nanete You'll have to perform a version check if your deployment target is <11.0 since safeAreaInsets doesn't exist before iOS 11.Gabriello
view.safeAreaInsets.bottom is coming as 0. And the space still exists for me.Terrieterrier
just found this issue no longer exist now at iOS12.Hawfinch
It does exist @HawfinchCud
The issue does still exist on iOS 15, and bottom is still 0. Even after calling safeAreaInsetsDidChange() (as some have suggested elsewhere).Ciri
L
2

My issue was that my view was in a child view controller. Converting the CGRect did the trick.

@objc private func keyboardWillChangeFrame(notification: NSNotification) {
    guard let userInfo = notification.userInfo, let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let duration: TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue else {
        return
    }

    let convertedFrame = view.convert(endFrame, from: nil)
    let endFrameY = endFrame.origin.y

    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 {
        inputContainerViewBottomLayoutConstraint.constant = 0.0
    }  else {
        var newHeight = view.bounds.size.height - convertedFrame.origin.y
        if #available(iOS 11.0, *) {
            newHeight = newHeight - view.safeAreaInsets.bottom
        }
        inputContainerViewBottomLayoutConstraint.constant = newHeight
    }

    UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: {
        self.view.layoutIfNeeded()
    },completion: { _ in
        self.scrollView.scrollToBottom()
    })
}
Lashing answered 17/1, 2019 at 3:49 Comment(0)
A
1

Swift 4/5 iPhone X I had to tweak Nathan's answer a little to get it to work. 100% this one does.

Note: Ensure you have control dragged from your bottom constraint for your textView/view from your storyboard to the bottom of your safe area in your ViewController, and that you have also control dragged and created an outlet in your target view controller's Project Navigator. I have named it bottomConstraint in my example. My text input field is wrapped in a view (MessageInputContainerView) to allow for additional send button alignment etc.

Constraint Demo

Here is the code:

@objc private func keyboardWillChange(_ notification: Notification) {
    guard let userInfo = (notification as Notification).userInfo, let value = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue else {
        return
    }
    
    if ((userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue) != nil {
        let isKeyboardShowing = notification.name == NSNotification.Name.UIKeyboardWillShow
        var newHeight: CGFloat
        if isKeyboardShowing {
            if #available(iOS 11.0, *) {
                newHeight = value.cgRectValue.height - view.safeAreaInsets.bottom
                bottomConstraint.constant = newHeight
                
            }
        }
        else {
            newHeight = value.cgRectValue.height
            bottomConstraint?.constant = view.safeAreaInsets.bottom + messageInputContainerView.bounds.height

        }
    }
}
Adman answered 20/8, 2018 at 8:53 Comment(1)
It isn't part of the code example and is for my own purpose. The code overrides the value of bottom constraint in both the if and else anyway.Adman
C
0

Make an outlet to your constraint to your ViewController. I'll refer to it as yourConstraint for now. Then add code to discover when the keyboard is being shown and when it is being dismissed. There you change the constant of the constraint accordingly. This allows you to keep using constraints.

In viewWillAppear:

NotificationCenter.default.addObserver(self, selector: #selector(YourViewController.keyboardWillShow), name:        NSNotification.Name.UIKeyboardWillShow, object: nil) // <=== Replace YourViewController with name of your view controller
NotificationCenter.default.addObserver(self, selector: #selector(YourViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil) // <=== Replace YourViewController with name of your view controller

In viewWillDisappear:

NotificationCenter.default.removeObserver(self)

In your UIViewController

@objc private func keyboardWillShow(notification: Notification) {
    guard let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
        yourConstraint.isActive = false // <=== 
        view.layoutIfNeeded()
        return
    }
    let duration: TimeInterval = ((notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue) ?? 0.4
    yourConstraint.constant = newValue // <===
    UIView.animate(withDuration: duration) {
        self.view.layoutIfNeeded()
    }
}

@objc private func keyboardWillHide(notification: Notification) {
    let duration: TimeInterval = ((notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue) ?? 0.4
    yourConstraint.constant = oldValue
    UIView.animate(withDuration: duration) {
        self.view.layoutIfNeeded()
    }
}

The UIView.animate is not necessary (the inside of the block is), but it makes the transition look nice.

Channelize answered 14/11, 2017 at 12:57 Comment(2)
I did the same. Problem is the newValue. I have added the newValue as keyboardSize.height which works perfect in all devices except iPhone X. As you can see in the first image there is white space for the below line new feature to close the application. I think that space is adding when keyboard appears.Nanete
I see. Sorry to duplicate your efforts... Can you maybe post a screenshot of your view hierarchy in your storyboard? Did you also try setting a different constraint just for iPhone X? You can differentiate between devices with something like this https://mcmap.net/q/44868/-how-to-determine-the-current-iphone-device-modelChannelize
S
0

The keyboard value reported by UIKeyboardFrameBeginUserInfoKey is different in these two cases in iPhone X:

  • Keyboard was not visible, a text field becomes first responder
  • Keyboard was already visible, a different text field becomes first responder.

To get the final height of the keyboard of the keyboard (including the safe area insets) use UIKeyboardFrameEndUserInfoKey.

In iOS 11 (iPhone X particularly), you may consider subtracting the safe area bottom insets.

    NSValue *keyboardEndFrameValue = notification.userInfo[UIKeyboardFrameEndUserInfoKey];
    if (keyboardEndFrameValue != nil) {
        CGRect keyboardSize = [keyboardEndFrameValue CGRectValue];
        _keyboardHeight = keyboardSize.size.height;
        if (@available(iOS 11.0, *)) {
            CGFloat bottomSafeAreaInset = self.view.safeAreaInsets.bottom;
            _keyboardHeight -= bottomSafeAreaInset;
        } else {
            // Fallback on earlier versions
        }
    }
Swats answered 10/6, 2018 at 5:39 Comment(0)
C
0

This worked for me

TL;DR: self.view.safeAreaInsets.bottom returned 0. The key was to use UIApplication.shared.keyWindow.safeAreaInsets.bottom instead [Source].

Let replyField be the UITextField of interest.

1) Add the observers in viewDidLoad().

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)

2) Set global variables within the class as the constraints.

var textFieldBottomConstraintKeyboardActive : NSLayoutConstraint?
var textFieldBottomConstraintKeyboardInactive : NSLayoutConstraint?

3) Set the constraints in a function called in viewDidLoad.

let replyFieldKeyboardActiveConstraint = self.replyField.bottomAnchor.constraint(
    equalTo: self.view.safeAreaLayoutGuide.bottomAnchor,
    constant: -1 * Constants.DEFAULT_KEYBOARD_HEIGHT /* whatever you want, we will change with actual height later */ + UITabBar.appearance().frame.size.height
)

let replyFieldKeyboardInactiveConstraint = self.replyField.bottomAnchor.constraint(
    equalTo: self.view.safeAreaLayoutGuide.bottomAnchor
)

self.textFieldBottomConstraintKeyboardActive = replyFieldKeyboardActiveConstraint
self.textFieldBottomConstraintKeyboardInactive = replyFieldKeyboardInactiveConstraint

self.textFieldBottomConstraintKeyboardActive?.isActive = false
self.textFieldBottomConstraintKeyboardInactive?.isActive = true

4) Define the keyboardWillShow and keyboardWillHide methods. They key here is how we define the heightOffset in the keyboardWillShow method.

@objc func keyboardWillShow(notification: NSNotification) {
    guard let userinfo = notification.userInfo else {
        return
    }
    guard let keyboardSize = userinfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
        return
    }
    let keyboardFrame = keyboardSize.cgRectValue

    self.view.layoutIfNeeded()

    let heightOffset = keyboardFrame.height - UITabBar.appearance().frame.height - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0)

    self.textFieldBottomConstraintKeyboardActive?.constant = -1 * heightOffset

    self.textFieldBottomConstraintKeyboardActive?.isActive = true
    self.textFieldBottomConstraintKeyboardInactive?.isActive = false

    self.view.setNeedsLayout()
}


@objc func keyboardWillHide(notification: NSNotification) {
    self.textFieldBottomConstraintKeyboardActive?.isActive = false
    self.textFieldBottomConstraintKeyboardInactive?.isActive = true
}
Cud answered 5/6, 2020 at 2:22 Comment(1)
This is indeed the correct answer. I used view.window.safeAreaInsets.bottom, same deal though.Ciri

© 2022 - 2024 — McMap. All rights reserved.