117 votes

SwiftUI : Comment faire en sorte que TextField devienne le premier intervenant ?

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 que lorsque le champ de texte devient visible pour que le champ de texte devienne le premier intervenant (c'est-à-dire qu'il reçoive le focus et que le clavier s'affiche).

14 votes

Le fait que quelque chose de basique comme cela ne soit même pas supporté montre à quel point SwiftUI est immature pour le moment. Regardez les solutions ci-dessous, c'est ridicule pour quelque chose qui est censé rendre le codage de l'interface graphique "plus simple".

86voto

Matteo Pacini Points 2704

Swift UI 3

À partir de Xcode 13, vous pouvez utiliser la fonction focused modificateur pour qu'une vue devienne premier répondant.


Swift UI 1/2

Cela ne semble pas possible pour le moment, mais vous pouvez mettre en œuvre quelque chose de similaire vous-même.

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
        }
    }
}

Nota: didBecomeFirstResponder est nécessaire pour s'assurer que le champ de texte devient le premier intervenant une seule fois, et non à 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 y a plus de choses qui se passent dans les coulisses.

En savoir plus Coordinators dans cet excellent exposé de la WWDC 19 : Intégration de SwiftUI

Testé sur Xcode 11.4

83voto

naturalethic Points 337

Utilisation de SwiftUI-Introspect que vous pouvez faire :

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

1 votes

Merci beaucoup pour votre aide. Il est bon de savoir que cela fonctionne également avec SecureField().

2 votes

Ce Cocoapod a aussi fonctionné pour moi. Je soupçonne que la seule raison pour laquelle ce n'était pas la réponse acceptée est que cette question a été posée et répondue en juin (WWDC19) bien avant la création de SwiftUI-Introspect (fin novembre 2019).

0 votes

Ce pod introspecteur est génial, il fonctionne parfaitement dans un téléphone réel, mais dans le simulateur, la vue entière ne répond plus si elle contient un champ de texte qui doit être le premier répondant.

26voto

Mojtaba Hosseini Points 2525

IOS 15

Il existe un nouveau wrapper appelé @FocusState qui contrôle l'état du clavier et du clavier ciblé ('aka' firstResponder).

Devenir premier répondant (ciblé)

Si vous utilisez un focused sur les champs de texte, vous pouvez les mettre en évidence :

enter image description here

Démissionner du poste de premier intervenant ( Renvoyer le clavier )

ou annuler le clavier en mettant la variable à nil :

enter image description here



iOS 13 et supérieur : Vieux mais fonctionnel !

Structure wrapper simple - fonctionne comme une structure native :

<strong>Notez que </strong>Le support de la reliure de texte a été ajouté comme demandé dans les commentaires

struct LegacyTextField: UIViewRepresentable {
    @Binding public var isFirstResponder: Bool
    @Binding public var text: String

    public var configuration = { (view: UITextField) in }

    public init(text: Binding<String>, isFirstResponder: Binding<Bool>, configuration: @escaping (UITextField) -> () = { _ in }) {
        self.configuration = configuration
        self._text = text
        self._isFirstResponder = isFirstResponder
    }

    public func makeUIView(context: Context) -> UITextField {
        let view = UITextField()
        view.addTarget(context.coordinator, action: #selector(Coordinator.textViewDidChange), for: .editingChanged)
        view.delegate = context.coordinator
        return view
    }

    public func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
        configuration(uiView)
        switch isFirstResponder {
        case true: uiView.becomeFirstResponder()
        case false: uiView.resignFirstResponder()
        }
    }

    public func makeCoordinator() -> Coordinator {
        Coordinator($text, isFirstResponder: $isFirstResponder)
    }

    public class Coordinator: NSObject, UITextFieldDelegate {
        var text: Binding<String>
        var isFirstResponder: Binding<Bool>

        init(_ text: Binding<String>, isFirstResponder: Binding<Bool>) {
            self.text = text
            self.isFirstResponder = isFirstResponder
        }

        @objc public func textViewDidChange(_ textField: UITextField) {
            self.text.wrappedValue = textField.text ?? ""
        }

        public func textFieldDidBeginEditing(_ textField: UITextField) {
            self.isFirstResponder.wrappedValue = true
        }

        public func textFieldDidEndEditing(_ textField: UITextField) {
            self.isFirstResponder.wrappedValue = false
        }
    }
}

Utilisation :

struct ContentView: View {
    @State var text = ""
    @State var isFirstResponder = false

    var body: some View {
        LegacyTextField(text: $text, isFirstResponder: $isFirstResponder)
    }
}

Bonus : Entièrement personnalisable

LegacyTextField(text: $text, isFirstResponder: $isFirstResponder) {
    $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 aquí

23voto

Rui Ying Points 669

Pour tous ceux qui ont abouti ici mais qui ont été confrontés à un plantage en utilisant la réponse de @Matteo Pacini, veuillez prendre connaissance de ce changement dans la 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 une sheet veuillez noter que vous ne pouvez pas appeler becomeFirstResponder jusqu'à ce que le champ de texte soit affiché. En d'autres termes, mettre le champ de texte de @Matteo Pacini directement en sheet le contenu provoque un crash.

Pour résoudre ce problème, ajoutez une vérification supplémentaire uiView.window != nil pour la visibilité du champ de texte. Il n'est mis en évidence qu'une fois qu'il se trouve dans la hiérarchie de la vue :

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 ?? ""
        }
    }
}

13voto

mahan Points 2519

IOS 15.0 et plus

macOS 12.0+,

Mac Catalyst 15.0+,

tvOS 15.0+,

watchOS 8.0 et plus

Utilice focused(_:) si vous avez un seul champ de texte.

concentré(_ :)

Modifie cette vue en liant son état d'accentuation à l'indicateur d'accentuation donné Booléen valeur de l'état.

struct NameForm: View {

    @FocusState private var isFocused: Bool

    @State private var name = ""

    var body: some View {
        TextField("Name", text: $name)
            .focused($isFocused)

        Button("Submit") {
            if name.isEmpty {
                isFocued = true
            }
        }
    }
}

Utilice focused(_:equals:) si vous avez plusieurs TextFields.

concentré(_:égal :)

Modifie cette vue en liant son état de focus à la valeur d'état donnée.

struct LoginForm: View {
    enum Field: Hashable {
        case usernameField
        case passwordField
    }

    @State private var username = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        Form {
            TextField("Username", text: $username)
                .focused($focusedField, equals: .usernameField)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .passwordField)

            Button("Sign In") {
                if username.isEmpty {
                    focusedField = .usernameField
                } else if password.isEmpty {
                    focusedField = .passwordField
                } else {
                    handleLogin(username, password)
                }
            }
        }
    }
}

Documentation sur SwiftUI


Mise à jour

Je l'ai testé dans xCode version 13.0 beta 5 (13A5212g) . Il fonctionne

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X