web-dev-qa-db-fra.com

SwiftUI: Comment faire de TextField le premier répondant?

Voici mon SwiftUI code:

struct ContentView : View {

    @State var showingTextField = false
    @State var text = ""

    var body: some View {
        return VStack {
            if showingTextField {
                TextField($text)
            }
            Button(action: { self.showingTextField.toggle() }) {
                Text ("Show")
            }
        }
    }
}

Ce que je veux, c'est quand le champ de texte devient visible, pour que le champ de texte devienne le premier répondant (c'est-à-dire recevoir le focus et faire apparaître le clavier).

46
Epaga

Cela ne semble pas possible pour le moment, mais vous pouvez implémenter vous-même quelque chose de similaire.

Vous pouvez créer un champ de texte personnalisé et ajouter une valeur pour qu'il devienne le premier répondant.

struct CustomTextField: UIViewRepresentable {

    class Coordinator: NSObject, UITextFieldDelegate {

        @Binding var text: String
        var didBecomeFirstResponder = false

        init(text: Binding<String>) {
            _text = text
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }

    }

    @Binding var text: String
    var isFirstResponder: Bool = false

    func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.delegate = context.coordinator
        return textField
    }

    func makeCoordinator() -> CustomTextField.Coordinator {
        return Coordinator(text: $text)
    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
        uiView.text = text
        if isFirstResponder && !context.coordinator.didBecomeFirstResponder  {
            uiView.becomeFirstResponder()
            context.coordinator.didBecomeFirstResponder = true
        }
    }
}

Remarque: didBecomeFirstResponder est nécessaire pour s'assurer que le champ de texte ne devient le premier répondant qu'une seule fois, pas à chaque rafraîchissement par SwiftUI!

Vous l'utiliseriez comme ça ...

struct ContentView : View {

    @State var text: String = ""

    var body: some View {
        CustomTextField(text: $text, isFirstResponder: true)
            .frame(width: 300, height: 50)
            .background(Color.red)
    }

}

P.S. J'ai ajouté un frame car il ne se comporte pas comme le stock TextField, ce qui signifie qu'il se passe plus de choses en coulisses.

En savoir plus sur Coordinators dans cet excellent exposé sur la WWDC 19: Integrating SwiftUI

Testé sur Xcode 11.1

38
Matteo Pacini

En utilisant https://github.com/timbersoftware/SwiftUI-Introspect on peut:

TextField("", text: $value)
.introspectTextField { textField in
    textField.becomeFirstResponder()
}

17
Joshua Kifer

Pour tous ceux qui se sont retrouvés ici mais qui se sont écrasés en utilisant la réponse de @Matteo Pacini, veuillez être conscient de ce changement dans la version bêta 4: Impossible d'assigner à la propriété: '$ text' est immuable à propos de ce bloc:

init(text: Binding<String>) {
    $text = text
}

devrait utiliser:

init(text: Binding<String>) {
    _text = text
}

Et si vous voulez que le champ de texte devienne le premier répondant dans un sheet, sachez que vous ne pouvez pas appeler becomeFirstResponder tant que le champ de texte n'est pas affiché. En d'autres termes, placer le champ de texte de @Matteo Pacini directement dans le contenu sheet provoque un crash.

Pour résoudre le problème, ajoutez une vérification supplémentaire uiView.window != nil pour la visibilité de textfield. Ne vous concentrez qu'une fois dans la hiérarchie des vues:

struct AutoFocusTextField: UIViewRepresentable {
    @Binding var text: String

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: UIViewRepresentableContext<AutoFocusTextField>) -> UITextField {
        let textField = UITextField()
        textField.delegate = context.coordinator
        return textField
    }

    func updateUIView(_ uiView: UITextField, context:
        UIViewRepresentableContext<AutoFocusTextField>) {
        uiView.text = text
        if uiView.window != nil, !uiView.isFirstResponder {
            uiView.becomeFirstResponder()
        }
    }

    class Coordinator: NSObject, UITextFieldDelegate {
        var parent: AutoFocusTextField

        init(_ autoFocusTextField: AutoFocusTextField) {
            self.parent = autoFocusTextField
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            parent.text = textField.text ?? ""
        }
    }
}
10
Rui Ying

Structure d'encapsuleur simple - Fonctionne comme un natif:

struct ResponderTextField: UIViewRepresentable {

    typealias TheUIView = UITextField
    var isFirstResponder: Bool
    var configuration = { (view: TheUIView) in }

    func makeUIView(context: UIViewRepresentableContext<Self>) -> TheUIView { TheUIView() }
    func updateUIView(_ uiView: TheUIView, context: UIViewRepresentableContext<Self>) {
        _ = isFirstResponder ? uiView.becomeFirstResponder() : uiView.resignFirstResponder()
        configuration(uiView)
    }
}

Usage:

struct ContentView: View {
    @State private var isActive = false

    var body: some View {
        ResponderTextField(isFirstResponder: isActive)
        // Just set `isActive` anyway you like
    }
}

???? Bonus: entièrement personnalisable

ResponderTextField(isFirstResponder: isActive) {
    $0.textColor = .red
    $0.tintColor = .blue
}

Cette méthode est entièrement adaptable. Par exemple, vous pouvez voir Comment ajouter un indicateur d'activité dans SwiftUI avec la même méthode ici

5
Mojtaba Hosseini

Comme d'autres l'ont noté (par exemple @ kipelovets commentent la réponse acceptée , par exemple @ réponse d'Eonil ), je n'ai également trouvé aucune des solutions acceptées pour travailler sur macOS. J'ai eu de la chance, cependant, d'utiliser NSViewControllerRepresentable pour obtenir un NSSearchField à apparaître comme premier répondeur dans une vue SwiftUI:

import Cocoa
import SwiftUI

class FirstResponderNSSearchFieldController: NSViewController {

  @Binding var text: String

  init(text: Binding<String>) {
    self._text = text
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func loadView() {
    let searchField = NSSearchField()
    searchField.delegate = self
    self.view = searchField
  }

  override func viewDidAppear() {
    self.view.window?.makeFirstResponder(self.view)
  }
}

extension FirstResponderNSSearchFieldController: NSSearchFieldDelegate {

  func controlTextDidChange(_ obj: Notification) {
    if let textField = obj.object as? NSTextField {
      self.text = textField.stringValue
    }
  }
}

struct FirstResponderNSSearchFieldRepresentable: NSViewControllerRepresentable {

  @Binding var text: String

  func makeNSViewController(
    context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
  ) -> FirstResponderNSSearchFieldController {
    return FirstResponderNSSearchFieldController(text: $text)
  }

  func updateNSViewController(
    _ nsViewController: FirstResponderNSSearchFieldController,
    context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
  ) {
  }
}

Exemple de vue SwiftUI:

struct ContentView: View {

  @State private var text: String = ""

  var body: some View {
    FirstResponderNSSearchFieldRepresentable(text: $text)
  }
}

0
David Muller

La réponse sélectionnée provoque un problème de boucle infinie avec AppKit. Je ne connais pas l'affaire UIKit.

Pour éviter ce problème, je recommande de partager directement l'instance NSTextField directement.

import AppKit
import SwiftUI

struct Sample1: NSViewRepresentable {
    var textField: NSTextField
    func makeNSView(context:NSViewRepresentableContext<Sample1>) -> NSView { textField }
    func updateNSView(_ x:NSView, context:NSViewRepresentableContext<Sample1>) {}
}

Vous pouvez l'utiliser comme ça.

let win = NSWindow()
let txt = NSTextField()
win.setIsVisible(true)
win.setContentSize(NSSize(width: 256, height: 256))
win.center()
win.contentView = NSHostingView(rootView: Sample1(textField: txt))
win.makeFirstResponder(txt)

let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.run()

Cela rompt la sémantique de la valeur pure, mais en fonction d'AppKit, vous abandonnez partiellement la sémantique de la valeur pure et allez vous offrir un peu de saleté. C'est un trou magique dont nous avons besoin en ce moment pour faire face au manque de contrôle des premiers intervenants dans SwiftUI.

Comme nous accédons directement à NSTextField, la définition du premier répondeur est une manière simple d'AppKit, donc aucune source visible de problème.

Vous pouvez télécharger le code source de travail ici .

0
Eonil