swiftswiftuiuiresponder

Handling UIResponder for UITextView in SwiftUI


I am trying to handle UIResponder for an existing subclass of UITextView in SwiftUI. I have been able to use the Coordinator pattern to handle UITextViewDelegate but I'm having trouble with UIResponder.

In the past (with UIKit), what I would have done is use NotificationCenter to add an observer for UIResponder.keyboardWillShowNotification in a subclass of UIViewController.

In SwiftUI, I'm not sure where to put it. I've done a simple thing which is to re-use the Coordinator class in makeUIView, as in:

    let nc = NotificationCenter.default
    nc.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: editorTextView)

But the keyboardWillShow method never gets called. Am I doing this wrong?


Solution

  • I would recommend using a Combine Publisher so you don't need to mess with selectors, but this should work either way. The observer/selector pattern is commented out in this example, but if you uncomment it both the observer and the publisher should respond when the keyboard appears.

        import Combine
        import SwiftUI
        import UIKit
    
        struct MyTextView: UIViewRepresentable {
        // Pass in the binding to the string from the SwiftUI view
        var text: Binding<String>
    
        init(text: Binding<String>) {
            self.text = text
        }
    
        func makeUIView(context: Context) -> UITextField {
            let tf = UITextField()
            tf.delegate = context.coordinator
            tf.text = context.coordinator.text.wrappedValue // Access the wrapped value in the binding
            return tf
        }
    
        func updateUIView(_ uiView: UITextField, context: Context) {
            //
        }
    
        func makeCoordinator() -> MyTextViewDelegate {
            let delegate = MyTextViewDelegate(text: text)
            return delegate
        }
    
        class MyTextViewDelegate: NSObject, UITextFieldDelegate {
    //      let nc = NotificationCenter.default
            var text: Binding<String>
    
            // You can use a Combine Publisher rather than dealing with selectors
            var subscriber: AnyCancellable?
    
            init(text: Binding<String>) {
                self.text = text
                super.init()
                subscriber = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
                    .sink() { [weak self] note in
                        print(self?.text.wrappedValue ?? "nil")
                        print("Publisher called -> " + note.description)
                }
    //            nc.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
            }
    
    
    //        @objc func keyboardWillShow(notification: Notification) {
    //            print("Selector called -> " + notification.description)
    //        }
    
            // Value should update in SwiftUI when return key is pressed to show that the data flows
            func textFieldShouldReturn(_ textField: UITextField) -> Bool {
                self.text.wrappedValue = textField.text ?? ""
                print(textField.text!)
                return true
            }
        }
    }