Search code examples
macosswiftuifocus

SwiftUI on AppKit - Set Focus to a TextField in a popover


In the AppKit example below I want the first Textfield to have the (keyboard)-focus when the popover appears.
How can I

  • set the initial focus to a View and
  • how can the focus order be controlled
  • how can I set the focus programmaticly , e.g. as an Button Action
struct ContentView: View {
  @State var showPopup = false
  @State var text = ""
  
  var body: some View {
    VStack{
      Text("Hello, world!")
      .padding()
      Button("show Popup") { showPopup = true}
    }
    .frame(width: 300, height: 300)
    .padding()
    .popover( isPresented: $showPopup,
              arrowEdge: .trailing
    ) {
      VStack{
        Text("Popup")
        TextField("enter Text",text:$text )
      }
      .padding()
    }
  }
}

Solution

  • I needed to do this in one of my projects myself. First, I subclassed NSViewController in which the view becomes the first responder (focused) when the view appears.

    class FocusedTextFieldViewController: NSViewController, NSTextFieldDelegate {
        @Binding var text: String
        
        init(text: Binding<String>) {
            _text = text
            super.init(nibName: nil, bundle: nil)
        }
        
        required init?(coder: NSCoder) {
            fatalError()
        }
    
        var textField: NSTextField!
        
        override func loadView() {
            // Set up the text field.
            textField = NSTextField()
            
            // Set the text field's delegate
            textField.delegate = self
            
            // Set an initial text value.
            textField.stringValue = text
            
            // Set the view to the new text field.
            view = textField
        }
        
        override func viewDidAppear() {
            // Make the view the first responder.
            view.window?.makeFirstResponder(view)
        }
        
        func controlTextDidChange(_ obj: Notification) {
            // Update the text.
            text = textField.stringValue
        }
    }
    

    Then, I used NSViewControllerRepresentable to display the NSViewController in SwiftUI.

    struct FocusedTextField: NSViewControllerRepresentable {
        @Binding var text: String
        
        func makeNSViewController(context: Context) -> some FocusedTextFieldViewController {
            return FocusedTextFieldViewController(text: $text)
        }
        
        func updateNSViewController(_ nsViewController: NSViewControllerType, context: Context) {
            let textField = nsViewController.textField
            textField?.stringValue = text
        }
    }
    

    Now, you can use the "FocusedTextField" in SwiftUI.

    struct ContentView: View {
        @State private var popoverIsPresented = false
        @State private var text = ""
        var body: some View {
            Button("Show Popover") {
                // Present the popover.
                popoverIsPresented.toggle()
            }
            .popover(isPresented: $popoverIsPresented, content: {
                FocusedTextField(text: $text)
            })
        }
    }
    

    If you want to unfocus the text field, you could add this to the loadView function to the FocusedTextFieldViewController and use Notification Center for unfocusing the text field again.

    override func loadView() {
        // Set up the text field.
        textField = NSTextField()
        
        // Set the text field's delegate
        textField.delegate = self
        
        // Set an initial text value.
        textField.stringValue = text
        
        // Set the view to the new text field.
        view = textField
        
        // Set up a notification for unfocusing the text field.
        NotificationCenter.default.addObserver(forName: Notification.Name("UnfocusTextField"), object: nil, queue: nil, using: { _ in
            // Unfocus the text field.
            self.view.window?.resignFirstResponder()
        })
    }
    

    Then you can execute this whenever you want to unfocus the text field.

    NotificationCenter.default.post(name: Notification.Name("UnfocusTextField"), object: nil)
    

    But this will probably not work if the text field is currently active.