Search code examples
iosswiftinterface-builderios-autolayoutsafearealayoutguide

Safe Area Insets Disappear when Moving View


In my iOS project, I have a view controller with a couple text fields. To ensure the controls stay visible when the user enters text I use the following code to move the content and make room for the keyboard:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    NotificationCenter.default.removeObserver(self)
}

@objc func keyboardWillShow(notification: NSNotification) {
    dPrint(tag: "Safe Area", message: "keyboardWillShow()")
    if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
        if activeField != nil {
            var activeFieldBottomDistance: CGFloat = view.frame.size.height - ... // calculation of position omitted here
            let yOffset: CGFloat = activeFieldBottomDistance
                                    - keyboardSize.height
                                    - view.frame.origin.y
            if yOffset + view.frame.origin.y < 0 {
                moveViewForKeyboard(offset: yOffset)
            }
        }
    }
}

@objc func keyboardWillHide(notification: NSNotification) {
    dPrint(tag: "Safe Area", message: "keyboardWillHide()")
    let yOffset: CGFloat = -view.frame.origin.y
    moveViewForKeyboard(offset: yOffset)
}

func moveViewForKeyboard(offset: CGFloat!) {
    let duration = 0.3
    UIView.beginAnimations(nil, context: nil)
    UIView.setAnimationBeginsFromCurrentState(true)
    UIView.setAnimationDuration(duration)
    view.frame = view.frame.offsetBy(dx: 0, dy: offset)
    UIView.commitAnimations()
    view.setNeedsLayout()
    view.layoutIfNeeded()
}

The layout is as follows (with the text views inside the stack view):

Layout

My problem is that when the root view is moved up to make room for the keyboard the safe area margins are lost. E.g. when using it in landscape and the keyboard appears the content expands horizontally, and when the keyboard hides again the content moves back into the bounds of the safe area.

The following code then produces the output below:

@available(iOS 11.0, *)
override func viewSafeAreaInsetsDidChange() {
    super.viewSafeAreaInsetsDidChange()
    dPrint(tag: "Safe Area", message: "saveAreaInsetsDidChange")
    dPrint(tag: "Safe Area", message: "top:    " + String(describing: view.safeAreaInsets.top))
    dPrint(tag: "Safe Area", message: "right:  " + String(describing: view.safeAreaInsets.right))
    dPrint(tag: "Safe Area", message: "bottom: " + String(describing: view.safeAreaInsets.bottom))
    dPrint(tag: "Safe Area", message: "left:   " + String(describing: view.safeAreaInsets.left))
}

Safe Area saveAreaInsetsDidChange
Safe Area top:    0.0
Safe Area right:  44.0
Safe Area bottom: 21.0
Safe Area left:   44.0
Safe Area keyboardWillShow()
Safe Area saveAreaInsetsDidChange
Safe Area top:    0.0
Safe Area right:  0.0
Safe Area bottom: 0.0
Safe Area left:   0.0
Safe Area keyboardWillHide()
Safe Area saveAreaInsetsDidChange
Safe Area top:    0.0
Safe Area right:  44.0
Safe Area bottom: 21.0
Safe Area left:   44.0

How can I move the content so that it stays within the safe area and that it works also on older devices starting with iOS9.

(One of the options I tried was to create an additional view layer below the root view and move that. This did not work because the content presented modally like a dialog box is centered vertically. Aligning the center with anything else than the root view does not allow the content to shift.)


Solution

  • You can use UnderKeyboard library for achieve that.

    So, I can strongly recommend use UIScrollView for moving a your content when a keyboard has been opened. Follow this guide.

    Or using an AutoLayout bottom constraint for bottom padding and animate changing its constant.

    Example:

    @IBOutlet var bottomConstraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "animateWithKeyboard:", name: UIKeyboardWillShowNotification, object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "animateWithKeyboard:", name: UIKeyboardWillHideNotification, object: nil)
    }
    
    func animateWithKeyboard(notification: NSNotification) {
    
        let userInfo = notification.userInfo!
        let keyboardHeight = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue().height
        let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! Double
        let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as! UInt
        let moveUp = (notification.name == UIKeyboardWillShowNotification)
    
        bottomConstraint.constant = moveUp ? -keyboardHeight : 0
    
        let options = UIViewAnimationOptions(rawValue: curve << 16)
        UIView.animateWithDuration(duration, delay: 0, options: options, animations: {
            self.view.layoutIfNeeded()
        },
        completion: nil)
    }