ScrollView and keyboard in Swift
Asked Answered
M

17

80

I started creating a simple iOS app that does some operations.

But I'm having some problems when the keyboard appears, hiding one of my textfields.

I think it's a common problem and I did some research but I couldn't find anything that solved my problem.

I want to use a ScrollView rather than animate the textfield to make it visible.

Margaritamargarite answered 1/11, 2014 at 12:18 Comment(0)
R
169

In ViewDidLoad, register the notifications:

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

Add below observer methods which does the automatic scrolling when keyboard appears.

@objc func keyboardWillShow(notification:NSNotification) {

    guard let userInfo = notification.userInfo else { return }
    var keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    keyboardFrame = self.view.convert(keyboardFrame, from: nil)

    var contentInset:UIEdgeInsets = self.scrollView.contentInset
    contentInset.bottom = keyboardFrame.size.height + 20
    scrollView.contentInset = contentInset
}

@objc func keyboardWillHide(notification:NSNotification) {

    let contentInset:UIEdgeInsets = UIEdgeInsets.zero
    scrollView.contentInset = contentInset
}
Redmund answered 15/9, 2015 at 10:33 Comment(8)
Using these code along with scrollView saved my life. Just add 20 contentInset.bottom to scroll it properly. contentInset.bottom = keyboardFrame.size.height + 20Bohaty
Works perfectly! The contentInset can be set to a 'let' instead of a 'var' in the keyboardWillHide though :)Experiential
This solution works when the UIControlView is scrolled.Sacristy
Binary operator '+' cannot be applied to operands of type 'UIEdgeInsets' and 'Double'Stein
First keyboardWillShow returns correct keyboard height (260). All next keyboardWillShow return keyboard height 216. Why?Savil
Do we need to remove the observers when ViewController get destroyed?Allocate
@BenShabat Apple Docs for NotificationCenter: If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its deallocation method.Test
You need to replace the UIResponder. keyboardFrameBeginUserInfoKey key with UIResponder.keyboardFrameEndUserInfoKey since we are interested in the final frame Keyboard will have as soon as it shows itself completelyClift
E
74

The top answer for swift 3:

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name:NSNotification.Name.UIKeyboardWillHide, object: nil)

And then:

func keyboardWillShow(notification:NSNotification){
    //give room at the bottom of the scroll view, so it doesn't cover up anything the user needs to tap
    var userInfo = notification.userInfo!
    var keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    keyboardFrame = self.view.convert(keyboardFrame, from: nil)

    var contentInset:UIEdgeInsets = self.theScrollView.contentInset
    contentInset.bottom = keyboardFrame.size.height
    theScrollView.contentInset = contentInset
}

func keyboardWillHide(notification:NSNotification){
    let contentInset:UIEdgeInsets = UIEdgeInsets.zero
    theScrollView.contentInset = contentInset
}
Enter answered 25/10, 2016 at 21:6 Comment(6)
Also maybe keep the textFieldShouldReturn method from above, if you want to use the return key for dismissing the keyboard. Thx for the update!Hedgepeth
Don't forget to NotificationCenter.default.removeObserver(self)Accumulate
Works like a charm. Thanks Daniel.Sharp
@Accumulate Apple Docs for NotificationCenter: If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its deallocation method.Paucker
I think you need replace UIKeyboardFrameBeginUserInfoKey with UIKeyboardFrameEndUserInfoKey in your answer. Because you need end frame in willShow method.Bureaucracy
The first time the keyboard opens, its height is zero for me. Afterwards it works fine, though. Do you know of any bugs related to this?Winnebago
W
42

Here is a complete solution, utilizing guard and concise code. Plus correct code in keyboardWillHide to only reset the bottom to 0.

@IBOutlet private weak var scrollView: UIScrollView!

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    registerNotifications()
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    scrollView.contentInset.bottom = 0
}

private func registerNotifications() {
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}

@objc private func keyboardWillShow(notification: NSNotification){
    guard let keyboardFrame = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
    scrollView.contentInset.bottom = view.convert(keyboardFrame.cgRectValue, from: nil).size.height
}

@objc private func keyboardWillHide(notification: NSNotification){
    scrollView.contentInset.bottom = 0
}
Welcome answered 15/7, 2017 at 21:21 Comment(6)
Nice answer. To be even more concise you can say ".UIKeyboardWillShow" instead of "NSNotification.Name.UIKeyboardWillShow". I updated your answer if you don't mind.Seda
UIKeyboardFrameBeginUserInfoKey is the wrong key; it should be UIKeyboardFrameEndUserInfoKey (begin → end). This solution gives zero height for the keyboard.Formicary
This Helped me! Thanks Mate :)Copolymer
Great answer. I think it would be safer to use viewDidDisappear() - if the view is navigated away whilst the keyboard is visible it is conceivable you could remove the notification observer before the hide has actually taken place, and therefore not reset your content inset. To be extra sure I'd also set the inset to 0 whenever the notification observer is removed.Polaroid
Pretty clean solution. In addition, keyboardWillShow and keyboardWillHide can be private.Ignaz
Thanks @Richard. I have updated my answer accordingly.Welcome
R
17

for Swift 4.0

In ViewDidLoad

// setup keyboard event
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

Add below observer methods which does the automatic scrolling when keyboard appears.

@objc func keyboardWillShow(notification:NSNotification){
    var userInfo = notification.userInfo!
    var keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    keyboardFrame = self.view.convert(keyboardFrame, from: nil)

    var contentInset:UIEdgeInsets = self.ui_scrollView.contentInset
    contentInset.bottom = keyboardFrame.size.height
    ui_scrollView.contentInset = contentInset
}

@objc func keyboardWillHide(notification:NSNotification){

    let contentInset:UIEdgeInsets = UIEdgeInsets.zero
    ui_scrollView.contentInset = contentInset
}
Remount answered 13/6, 2018 at 5:16 Comment(1)
For Swift 5, NSNotification.Name.UIKeyboardWillShow is UIResponder.keyboardWillShowNotification and UIKeyboardFrameBeginUserInfoKey is UIResponder.keyboardFrameBeginUserInfoKey.Asymmetric
A
11

contentInset doesn't work for me, because I want the scrollview move all the way up above the keyboard. So I use contentOffset:

func keyboardWillShow(notification:NSNotification) {
    guard let keyboardFrameValue = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue else {
        return
    }
    let keyboardFrame = view.convert(keyboardFrameValue.cgRectValue, from: nil)
    scrollView.contentOffset = CGPoint(x:0, y:keyboardFrame.size.height)
}

func keyboardWillHide(notification:NSNotification) {
    scrollView.contentOffset = .zero
}
Accumulate answered 21/3, 2017 at 0:1 Comment(0)
O
10

Swift 5 Only adjust ScrollView when TextField is hidden by keyboard (for multiple TextFields)

Add / Remove Observers:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillChangeFrameNotification, 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)
}

Keep track of these values so you can return to your original position:

var scrollOffset : CGFloat = 0
var distance : CGFloat = 0

Adjust ScrollView contentOffset depending on TextField Location:

@objc func keyboardWillShow(notification: NSNotification) {
    if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {

        var safeArea = self.view.frame
        safeArea.size.height += scrollView.contentOffset.y
        safeArea.size.height -= keyboardSize.height + (UIScreen.main.bounds.height*0.04) // Adjust buffer to your liking

        // determine which UIView was selected and if it is covered by keyboard

        let activeField: UIView? = [textFieldA, textViewB, textFieldC].first { $0.isFirstResponder }
        if let activeField = activeField {
            if safeArea.contains(CGPoint(x: 0, y: activeField.frame.maxY)) {
                print("No need to Scroll")
                return
            } else {
                distance = activeField.frame.maxY - safeArea.size.height
                scrollOffset = scrollView.contentOffset.y
                self.scrollView.setContentOffset(CGPoint(x: 0, y: scrollOffset + distance), animated: true)
            }
        }
        // prevent scrolling while typing

        scrollView.isScrollEnabled = false
    }
}
@objc func keyboardWillHide(notification: NSNotification) {
        if distance == 0 {
            return
        }
        // return to origin scrollOffset
        self.scrollView.setContentOffset(CGPoint(x: 0, y: scrollOffset), animated: true)
        scrollOffset = 0
        distance = 0
        scrollView.isScrollEnabled = true
}

Make sure to use [UIResponder.keyboardFrameEndUserInfoKey] to get the proper keyboard height the first time.

Orwin answered 21/8, 2019 at 1:26 Comment(0)
S
7

From the answer by Sudheer Palchuri, converted for Swift 4.

In ViewDidLoad, register the notifications:

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name:NSNotification.Name.UIKeyboardWillHide, object: nil)

And then:

// MARK: - Keyboard Delegates
func textFieldShouldReturn(textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
}

@objc func keyboardWillShow(notification:NSNotification){

    var userInfo = notification.userInfo!
    var keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    keyboardFrame = self.view.convert(keyboardFrame, from: nil)

    var contentInset:UIEdgeInsets = self.scrollView.contentInset
    contentInset.bottom = keyboardFrame.size.height
    self.scrollView.contentInset = contentInset
}

@objc func keyboardWillHide(notification:NSNotification){

    let contentInset:UIEdgeInsets = UIEdgeInsets.zero
    self.scrollView.contentInset = contentInset
}
Substage answered 9/3, 2018 at 4:36 Comment(0)
M
5

Reading the links you sent to me, I found a way to make it work, thanks!:

func textFieldDidBeginEditing(textField: UITextField) {            
    if (textField == //your_field) {
        scrollView.setContentOffset(CGPointMake(0, field_extra.center.y-280), animated: true)
        callAnimation()
        viewDidLayoutSubviews()
    }
}

func textFieldDidEndEditing(textField: UITextField) {    
    if (textField == //your_field){
        scrollView .setContentOffset(CGPointMake(0, 0), animated: true)
        viewDidLayoutSubviews()
    }
}
Margaritamargarite answered 1/11, 2014 at 22:21 Comment(0)
B
3

This is a refinement on Zachary Probst solution posted above. I ran into a few issues with his solution and fixed it and enhanced it a bit.

This version does not need to pass in a list of UITextView controls. It finds the first responder in the current view. It also handles UITextView controls at any level in the View hierarchy.

I think his safeArea calculation wasn't quite right scrollView.contentOffset.y needed sign changed. It didn't show up if it was not scrolled. This fixed incremental scrolling. It might have been from other changes I made.

This works if the user jumps around to other UITextViews while the keyboard is up.

This is a Base Class I use for a bunch of ViewControllers. The inherited ViewController just needs to set the UIScrollViewer which activates this code behavior.

class ThemeAwareViewController: UIViewController
{
    var scrollViewForKeyBoard: UIScrollView? = nil
    var saveOffsetForKeyBoard: CGPoint?
    
    func findViewThatIsFirstResponder(view: UIView) -> UIView?
    {
        if view.isFirstResponder {
            return view
        }

        for subView in view.subviews {
            if let hit = findViewThatIsFirstResponder(view: subView) {
                return hit
            }
        }

        return nil
    }
    
    @objc func keyboardWillShow(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            if let scrollView = scrollViewForKeyBoard {
                
                var safeArea = self.view.frame
                safeArea.size.height -= scrollView.contentOffset.y
                safeArea.size.height -= keyboardSize.height
                safeArea.size.height -= view.safeAreaInsets.bottom
                
                let activeField: UIView? = findViewThatIsFirstResponder(view: view)
                
                if let activeField = activeField {
                    // This line had me stumped for a while (I was passing in .frame)
                    let activeFrameInView = view.convert(activeField.bounds, from: activeField)
                    let distance = activeFrameInView.maxY - safeArea.size.height
                    if saveOffsetForKeyBoard == nil {
                        saveOffsetForKeyBoard = scrollView.contentOffset
                    }
                    scrollView.setContentOffset(CGPoint(x: 0, y: distance), animated: true)
                }
            }
        }
    }
    
    @objc func keyboardWillHide(notification: NSNotification) {
        guard let restoreOffset = saveOffsetForKeyBoard else {
            return
        }
        if let scrollView = scrollViewForKeyBoard {
            scrollView.setContentOffset(restoreOffset, animated: true)
            self.saveOffsetForKeyBoard = nil
        }
    }   
}
Bradwell answered 19/9, 2021 at 2:10 Comment(1)
Awesome solution, man. Works like a charm. But you really, REALLY should have mentioned about NotificationCenter.default.addObserver first. Maybe that's why you still didn't get any likes!Aparicio
S
2

You can animate your scrollview to center on your UITextField on keyboard appearance (ie. making your textfield the first responder) via a scroll offset. Here are a couple of good resources to get you started (there are a bunch on this site):

How programmatically move a UIScrollView to focus in a control above keyboard?

How to make a UIScrollView auto scroll when a UITextField becomes a first responder

Additionally, if you simply use a UITableView with your content in cells, when the textfield becomes first responder, the UITableViewController will automatically scroll to the textfield cell for you (though I'm not sure this is what you want to do).

Swing answered 1/11, 2014 at 15:43 Comment(0)
A
2

An answer for Swift 3, based on the one proposed by Daniel Jones, but safer (thanks to the guard), more concise and with consistent scroll indicator insets:

@objc private func keyboardWillBeShown(notification: NSNotification) {
    guard let keyboardFrameValue = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue else { return }
    let keyboardFrame = view.convert(keyboardFrameValue.cgRectValue, from: nil)
    scrollView.contentInset.bottom = keyboardFrame.size.height
    scrollView.scrollIndicatorInsets = scrollView.contentInset
}

@objc private func keyboardWillBeHidden() {
    scrollView.contentInset = .zero
    scrollView.scrollIndicatorInsets = scrollView.contentInset
}
Algebra answered 26/1, 2017 at 13:33 Comment(2)
I get ViewController has no member for self.scrollViewBoiler
@Boiler That's because you need to create IBOutlet for your scrollviewCuesta
S
2

In Swift4, just add the following extension.

extension UIViewController {

   func setupViewResizerOnKeyboardShown() {
        NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.keyboardWillShowForResizing),
                                           name: 
        Notification.Name.UIKeyboardWillShow,
                                           object: nil)
        NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.keyboardWillHideForResizing),
                                           name: Notification.Name.UIKeyboardWillHide,
                                           object: nil)
        }

   @objc func keyboardWillShowForResizing(notification: Notification) {
        if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
        let window = self.view.window?.frame {
        // We're not just minusing the kb height from the view height because
        // the view could already have been resized for the keyboard before
        self.view.frame = CGRect(x: self.view.frame.origin.x,
                                 y: self.view.frame.origin.y,
                                 width: self.view.frame.width,
                                 height: window.origin.y + window.height - keyboardSize.height)
      } else {
        debugPrint("We're showing the keyboard and either the keyboard size or window is nil: panic widely.")
      }
  }

   @objc func keyboardWillHideForResizing(notification: Notification) {
     if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
        let viewHeight = self.view.frame.height
        self.view.frame = CGRect(x: self.view.frame.origin.x,
                                 y: self.view.frame.origin.y,
                                 width: self.view.frame.width,
                                 height: viewHeight + keyboardSize.height)
    } else {
        debugPrint("We're about to hide the keyboard and the keyboard size is nil. Now is the rapture.")
    }
   }
 }
Sacristy answered 22/5, 2018 at 17:35 Comment(0)
G
1

Intro

It's a pretty common thing we have to work with during the development process. Conceptually, if any view inside UIScrollView gets covered by the system keyboard, you have to follow a few steps to make it work perfectly:

  1. Write a callback function (selector) that will handle keyboard appearance
  2. Write a callback function (selector) that will handle keyboard dissapearance
  3. Write function to subscribe for keyboard system events (appearance and dissapearance) using written selectors (from step 1 and step 2)
  4. Write function to unsubscribe from keyboard system events
  5. Add subscription (step 3) and unsubscription (step 4) methods in appropriate view-life-cycle methods

Let's dive into

Disclaimer

The most important part to answer clearly is providing conception that you can adapt in your real code environment with any architecture you have. So, next steps assume that we have some custom UIViewController that has access to some custom UIView with UIScrollView and your accidentally covered UI (any UIView) inside UIScrollView.

How get we know about keyboard events?

NotificationCenter is a dispatch mechanism that allows us to subscribe for any information we need as observers and, luckily, for keyboard events too. So, our custom UIViewController will be an observer for keyboard events from NotificationCenter. NotificationCenter uses notifications (NSNotification) as a way of broadcasting that contains some userInfo with useful information inside. That's what we'll work with. So, let's dive into implementation finally!

Implementation

Step 1

We need to write a selector that will be used for handling income notification that will come each time the keyboard appears to fetch all the neccesary data from event:


import UIKit
import os


final class Controller: UIViewController {

    // let's assume it's your custom view
    // as instance of class `View` with
    // `UIScrollView` inside and your covered UI as well:
    private let body: View

    // step 1:
    @objc
    private func willShowKeyboard(from notification: NSNotification) {
        // #1
        guard let screen = notification.object as? UIScreen else {
            Logger().error("\(#function) invalid screen object in notification")
            return
        }
        // #2
        guard let userInfo = notification.userInfo else {
            Logger().error("\(#function) no user info in notification")
            return
        }
        // #3
        guard let frameEnd = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
            Logger().error("\(#function) no keyboard frame in user info from notification")
            return
        }
    
        // #4
    
        let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey]
        let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey]

        guard
            let animationDuration = duration as? Double,
            let animationCurve = curve as? UInt
        else {
            Logger().error("\(#function) no correct animation duration or curve in user info from notification")
            return
        }
    
        // #5

        let viewCoordinateSpace: UICoordinateSpace = self.body
        let globalCoordinatesSpace: UICoordinateSpace = screen.coordinateSpace
        let keyboardFrame = globalCoordinateSpace.convert(frame.cgRectValue, to: viewCoordinateSpace)

        // #6

        let textFieldFrame = self.body.textField.frame
        let keyboardMinY = keyboardFrame.origin.y
    
        // let's assume it's a normal space between
        // keyboard and the bottom edge of text field:
        let keyboardTopMargin: CGFloat = 20

        let haveToBeVisibleY = textFieldFrame.origin.y + textFieldFrame.height + keyboardTopMargin
        let yDifference = haveToBeVisibleY - keyboardMinY

        // is bottom edge of the text field visually lower than a top edge of the keyboard?

        if yDifference > .zero {
            // #7
            let options: UIView.AnimationOptions = .init(rawValue: animationCurve << 16)
        
            // #8
            let offsetBefore = self.body.scrollView.contentOffset
            let newOffset = CGPoint(x: offsetBefore.x, y: offsetBefore.y + offset)
        
            // #9
            UIView.animate(withDuration: animationDuration, delay: .zero, options: options) {
                self.scrollView.contentOffset = newOffset
            }
        }
    }
}

Let me try explain the code upper with a few more details below:

  1. We're trying to retrieve object as an UIScreen instance that later we use in step #
  2. As any usual notification contains of userInfo, we're trying to retrieve a hasmap of useful information
  3. We're retrieving a frame of the keyboard after animation will be completed. If you want to consider a frame of the keyboard at the start position before animation performing, check a key by using UIResponder.keyboardFrameBeginUserInfoKey.
  4. Then we retrieve duration and curve of the future keyboard appearance animation and then make type casting for them to adopt for our usage
  5. One of the most important parts why I decided to write this answer: keyboard's frame is not always in the same coordinate system comparing to your UI's coordinate system. That is why we did step #1 in the code above. We take both coordinate systems and convert keyboard's coordinate system into our system to get a valid keyboard frame to avoid calculation errors!
  6. Then we want to realize whether the bottom edge of our UI (text field, for instance but it can be anything we want) is visually lower than the top edge of the keyboard including keyboard's top margin (you may avoid top margin or set your own appropriate value to improve UI visually). For this reason, we calculate max Y of text field (including keyboard's top margin space) and min Y of the keyboard's frame because vertical axis system starts from the top of the screen due to the core logic. And, if we the max Y of the text field is bigger than the min Y of the keyboard, than we need to add inset for our scroll view to scroll it up for an appropriate Y points (yDifference).
  7. Based on step 4, we can add content offset for scroll view with the same animation and at the same time as a keyboard will appear. That's why retrieved animation duration and animation curve to immitate the keyboard's system animation. But the curve we've got from user info is a raw 32-bit unsigned integer value in fact, so to adopt our raw curve value to UIView.AnimationOptions's value we need to shift left our raw value by 16 bits to avoid zeroes on the left of the range of bits in raw value.
  8. Alright, all we gotta do is just to calculate a new offset based on current offset of scroll view. We just add a new offset by Y axis to the current one.
  9. And inside the block with all necessary and correct options we perform setting a new offset for scroll view animationally smoothly as user expected by default.

Don't worry, if you read it, all the next steps will be much easier.

Step 2

As we've written a function that handles keyboard appearance, we also need to write a function that scrolls the content back each time the keyboard will dissapear:


import UIKit
import os


final class Controller: UIViewController {

    // some code from step 1

    @objc
    private func willHideKeyboard(from notification: NSNotification) {
        // or any other value such a previous value before the keyboard has appeared.
        self.body.scrollView.contentOffset = .zero
    }
}

Step 3

Alright, since we've written the core logic when the keyboard appears and dissapears, all we gotta do is just add these selectors to the notification center:

import UIKit
import os

final class Controller: UIVIewController {

    /*
    some code from step 1, 2
    */

    private func subscribeForKeyboardEvents() {
        self.subscribeForKeyboardAppearance()
        self.subscribeForKeyboardDissapearance()
    }

    private func subscribeForKeyboardAppearance() {
        let selector = #selector(self.willShowKeyboard(from:))
        let name = UIResponder.keyboardWillShowNotification
        
        NotificationCenter
            .default
            .addObserver(self, selector: selector, name: name, object: nil)
    }

    private func subscribeForKeyboardDissapearance() {
        let selector = #selector(self.willHideKeyboard(from:))
        let name = UIResponder.keyboardWillHideNotification
        
        NotificationCenter
            .default
            .addObserver(self, selector: selector, name: name, object: nil)
    }
}

Step 4

And if the view controller and its view will be destroyed from memory, it would be great to consider prepared methods to unsubscribe from keyboard events in a similar way as we did in the previous step but vice-versa:

// some code from steps 1, 2 and 3

private func unsubscribeFromKeyboardEvents() {
    self.unsubscribeFromKeyboardAppearance()
    self.unsubscribeFromKeyboardDisappearance()
}
    
private func unsubscribeFromKeyboardAppearance() {
    let name = UIResponder.keyboardWillShowNotification
        
    NotificationCenter
        .default
        .removeObserver(self, name: name, object: nil)
}   

private func unsubscribeFromKeyboardDisappearance() {
    let name = UIResponder.keyboardWillHideNotification
    
    NotificationCenter
        .default
        .removeObserver(self, name: name, object: nil)
}

Step 5

We just call functions from steps 3 and 4 in appropriate view-life-cycle methods as below:


final class Controller: UIViewController {
    
    // code from previous steps

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.subscribeForKeyboardEvents()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        self.unsubscribeFromKeyboardEvents()
    }
}

Get interested?

Read more in the official documentation by Apple:

Gentleness answered 20/2, 2024 at 12:5 Comment(0)
D
0
override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)

    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

}

func keyboardWillShow(_ notification:Notification) {

    if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
        tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0)
    }
}
func keyboardWillHide(_ notification:Notification) {

    if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
        tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
    }
}
Detumescence answered 10/3, 2017 at 10:14 Comment(0)
O
0

In case anyone is looking for Objective-C code for this solution:

- (void)keyboardWasShown:(NSNotification *)notification {
        NSDictionary* info = [notification userInfo];
        CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
        UIEdgeInsets contentInsets = baseScrollView.contentInset;
        contentInsets.bottom = kbSize.height;
        baseScrollView.contentInset = contentInsets;
    }

    - (void)keyboardWillBeHidden:(NSNotification *)notification {
        UIEdgeInsets contentInsets = UIEdgeInsetsZero;
        baseScrollView.contentInset = contentInsets;
        [baseScrollView endEditing:YES];
    }
Onieonion answered 5/7, 2018 at 12:27 Comment(0)
L
0

In my situation it was

ScrollView --> TableView --> TableViewCell

So I had to get y position in relative to keyboard frame and check if keyboard y position and my active field y position was intersecting or not

   @objc func keyboardWillShow(_ notification: Foundation.Notification) {

        var userInfo = notification.userInfo!
        var keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
        keyboardFrame = self.view.convert(keyboardFrame, from: nil)

        var contentInset:UIEdgeInsets = self.scrollView!.contentInset
        contentInset.bottom = keyboardFrame.size.height
        let loc = self.activeTextField?.convert(activeTextField!.bounds, to: self.view)

        if keyboardFrame.origin.y <  loc!.origin.y {
            self.scrollView?.contentOffset = CGPoint.init(x: (self.scrollView?.contentOffset.x)!, y: loc!.origin.y)
        }

        if self.scrollView?.contentInset.bottom == 0 {
            self.scrollView?.contentInset = contentInset
        }

    }
Loose answered 4/12, 2019 at 7:35 Comment(0)
C
0

There is a simple solution here

Curley answered 1/8, 2022 at 2:37 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.