xcodelistnavigationswiftui

Show/hide navigationBarItems when keyboard visible/non-visible | SwiftUI


I've spent a lot of time researching and playing with different codes to make this work but can't seem to figure it out. Below you'll see two pieces of code, one of me creating the "Done" bar button item and the other is checking to see if keyboard is present and to close it when the button is clicked. The one issue I'm having is, I only want the bar button to show when the keyboard is present. I want the bar button hidden once the keyboard is gone or prior to even opening up the keyboard in the first place. How do you do that nowadays with SwiftUI?

.navigationBarItems(trailing:
            Button(action: {
                if UIApplication.shared.isKeyboardPresented {
                    UIApplication.shared.endEditing()
                }
            }, label: {
                Text("Done")
            })

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
    /// Checks if view hierarchy of application contains `UIRemoteKeyboardWindow` if it does, keyboard is presented
    var isKeyboardPresented: Bool {
        if let keyboardWindowClass = NSClassFromString("UIRemoteKeyboardWindow"),
            self.windows.contains(where: { $0.isKind(of: keyboardWindowClass) }) {
            return true
        } else {
            return false
        }
    }
}

Solution

  • As an option, you can define an object to listen to keyboard notifications:

    class KeybordManager: ObservableObject {
        static let shared = KeybordManager()
    
        @Published var keyboardFrame: CGRect? = nil
    
        init() {
            let notificationCenter = NotificationCenter.default
            notificationCenter.addObserver(self, selector: #selector(willHide), name: UIResponder.keyboardWillHideNotification, object: nil)
            notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
        }
    
        @objc func willHide() {
            self.keyboardFrame = .zero
        }
    
        @objc func adjustForKeyboard(notification: Notification) {
            guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
    
            let keyboardScreenEndFrame = keyboardValue.cgRectValue
            self.keyboardFrame = keyboardScreenEndFrame
        }
    }
    

    Then inside your view subscribe to keyboardFrame updates and show and hide the Done button accordingly:

    public struct ContentView: View {
    
        @State private var showDoneButton = false
        @State private var text = ""
    
        public var body: some View {
            NavigationView {
                TextField("Some text", text: $text)
                .navigationBarItems(trailing:
                    Group {
                        if self.showDoneButton {
                            Button(action: {
                                UIApplication.shared.endEditing()
                            }, label: {
                                Text("Done")
                            })
                        } else {
                            EmptyView()
                        }
                }
    
                )
                    .onReceive(KeybordManager.shared.$keyboardFrame) { keyboardFrame in
                        if let keyboardFrame = keyboardFrame, keyboardFrame != .zero {
                            self.showDoneButton = true
                        } else {
                            self.showDoneButton = false
                        }
                }
            }
        }
    }
    
    extension UIApplication {
        func endEditing() {
            sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
    }