P175. The keyboard handling code does not handle safe area well + improvements

The code for keyboard handling could be improved, I found the following issues:

  1. (bug) The padding is too large when keyboard is shown on phones with safe areas. The problem is that the keyboard height includes the safe area padding, but the view pads from the safe area.
    I submit a solution where the bottom safe area edge is ignored if keyboard is shown.

  2. (improvement) The way the KeyboardFollower is passed to the view — I think that the use of @EnvironmentObject is much better, you will probably use the KeyboardFollower in multiple views in an app. Instead of passing around one or many instances of KeyboardFollower it’s much better to create one instance serving the complete app.

  3. (improvement) The KeyboardFollower implementation. Why not just start the observing when the class in init’ed? The code will be much cleaner, no need for subscribe/unsubscribe in the class, and each view does not need to have .onAppear/.onDisappear handlers clogging down the code.

  4. (bug) If two views is using the same instance of current KeyboardFollower implementation, the first view disappearing will disable further notifications to the remaining view.

This is my suggested code improvements (a working example):

In SceneDelegate.swift change to:

            window.rootViewController = UIHostingController(rootView: contentView
                .environmentObject(KeyboardFollower())
            )

ContentView.swift:

struct ContentView: View {
    @State var text: String = ""
    @EnvironmentObject var keyboardObserver: KeyboardFollower
    
    var body: some View {
        VStack {
            TextField("Type something", text: $text)
                .padding()
            List {
                ForEach(1..<25) { n in
                    Text("\(n). Text: '\(self.text)'")
                }
            }
        }
        .padding(.bottom, keyboardObserver.keyboardHeight)
        .edgesIgnoringSafeArea(keyboardObserver.isVisible ? .bottom : [])
    }
}

KeyboardFollower.swift:

class KeyboardFollower : ObservableObject {
    @Published var keyboardHeight: CGFloat = 0
    @Published var isVisible = false
    
    init() {
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardVisibilityChanged), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    }
    
    @objc private func keyboardVisibilityChanged(_ notification: Notification) {
        guard let userInfo = notification.userInfo else { return }
        guard let keyboardBeginFrame = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { return }
        guard let keyboardEndFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        isVisible = keyboardBeginFrame.minY > keyboardEndFrame.minY
        keyboardHeight = isVisible ? keyboardEndFrame.height : 0
    }
}

Sorry about the space after each @, I’m not allowed to post this message otherwise. (New user can not address more than 2 users in a post). Wierd…

Edit: Was fixed by editing the post after publishing :slight_smile:

2 Likes

@audrey
(bug) I found another issue with the KeyboardFollower implementation.

If you have the keyboard visible while leaving the app for another app (or springboard) the reported height is wrong when coming back to the app again.

This is my updated code:

    @objc private func keyboardVisibilityChanged(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let endFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        
        isVisible = endFrame.minY < UIScreen.main.bounds.height
        keyboardHeight = isVisible ? endFrame.height : 0
    }
1 Like

thanks Jens! paging @jeden :smile:

Thanks a lot @jkufver, I’ll take a look today or tomorrow.

1 Like

Hi @jkufver, thanks again for your feedback.

I tried your suggestion, but I couldn’t notice any visible difference - but your idea seems a good one, so I’ll test it again in the future.

I agree, but also disagree :]. The reason is that I prefer the keyboard follower to not be shared - I think that each view should have its own instance, so that’s why I chose to not pass it as an env object.

Another good suggestion. I’ve just improved your implementation a little bit, by unsubscribing in deinit

That’s one of the reasons why I prefer not not share the keyboard follower. :]

You right - and your fix works!

Thank you very much for your help. Your suggestions and fixes will be available in the next book version.

Hi and thank you for taking you the time to respond.

#1. I send you a screenshot displaying the error and my fix.

wrong%20padding

correct%20padding

#2. Fair enought. Add to you article why you prefer that over a env object :slight_smile: (I see no benefits)

#3. Yes, unscribing in deinit is necessary if you have multiple instances of the class

#4. If you only have one instance this is not a problem in my implementation :slight_smile:

I enjoy helping you.

Keep up the good work

Chers.

1 Like

Thanks for this information. It is useful

Thanks a lot, and sorry for replying so late. I tested that, but I am unable to replicate it. I guess I have to do more testing.

I’m working thru this book now with Swift 5.3 and Xcode 12.2. The view seems to adjust itself automatically when the keyboard appears, without using KeyboardFollower. Are they handling this automatically now?

Hi @snackdog,

it looks like you’re right - I didn’t notice that, but I’ve tested and verified that it works without having to use a custom keyboard handler.

Thanks for pointing that out. I’ll dig deeper.

Antonio

Cool. I’d love to hear what you find out.