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".

2voto

SomaMan Points 2215

Il ne s'agit pas vraiment d'une réponse, mais simplement d'un développement de l'excellente solution de Casper avec un modificateur pratique.

struct StartInput: ViewModifier {

    @EnvironmentObject var chain: ResponderChain

    private let tag: String

    init(tag: String) {
        self.tag = tag
    }

    func body(content: Content) -> some View {

        content.responderTag(tag).onAppear() {
            DispatchQueue.main.async {
                chain.firstResponder = tag
            }
        }
    }
}

extension TextField {

    func startInput(_ tag: String = "field") -> ModifiedContent<TextField<Label>, StartInput> {
        self.modifier(StartInput(tag: tag))
    }
}

Il suffit d'utiliser comme -

TextField("Enter value:", text: $quantity)
    .startInput()

1voto

Eonil Points 19404

La réponse choisie cause un problème de boucle infinie avec AppKit. Je ne connais pas le cas de UIKit.

Pour éviter ce problème, je recommande de partager 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 utiliser ça 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 brise la sémantique de valeur pure, mais dépendre d'AppKit signifie que vous abandonnez partiellement la sémantique de valeur pure et que vous allez vous permettre une certaine saleté. C'est un trou magique dont nous avons besoin en ce moment pour faire face au manque de contrôle du premier répondant dans SwiftUI.

Comme nous accédons NSTextField directement, le réglage du premier répondant se fait à la manière d'AppKit, donc sans source de problème visible.

Vous pouvez télécharger le code source fonctionnel aquí .

0voto

Anshuman Singh Points 11

Puisque la chaîne de réponse n'est pas disponible pour être consommée via SwiftUI, nous devons la consommer en utilisant UIViewRepresentable.

Jetez un coup d'œil au lien ci-dessous car j'ai créé une solution de contournement qui peut fonctionner de manière similaire à ce que nous avions l'habitude de faire avec UIKit.

https://stackoverflow.com/a/61121199/6445871

0voto

C'est ma variante de l'implémentation, basée sur les solutions de @Mojtaba Hosseini et @Matteo Pacini. Je suis encore novice en matière de SwiftUI, je ne peux donc pas garantir l'exactitude absolue du code, mais il fonctionne.

J'espère que cela pourra être utile à quelqu'un.

ResponderView : Il s'agit d'une vue générique de répondeur, qui peut être utilisée avec n'importe quelle vue UIKit.

struct ResponderView<View: UIView>: UIViewRepresentable {
    @Binding var isFirstResponder: Bool
    var configuration = { (view: View) in }

    func makeUIView(context: UIViewRepresentableContext<Self>) -> View { View() }

    func makeCoordinator() -> Coordinator {
        Coordinator($isFirstResponder)
    }

    func updateUIView(_ uiView: View, context: UIViewRepresentableContext<Self>) {
        context.coordinator.view = uiView
        _ = isFirstResponder ? uiView.becomeFirstResponder() : uiView.resignFirstResponder()
        configuration(uiView)
    }
}

// MARK: - Coordinator
extension ResponderView {
    final class Coordinator {
        @Binding private var isFirstResponder: Bool
        private var anyCancellable: AnyCancellable?
        fileprivate weak var view: UIView?

        init(_ isFirstResponder: Binding<Bool>) {
            _isFirstResponder = isFirstResponder
            self.anyCancellable = Publishers.keyboardHeight.sink(receiveValue: { [weak self] keyboardHeight in
                guard let view = self?.view else { return }
                DispatchQueue.main.async { self?.isFirstResponder = view.isFirstResponder }
            })
        }
    }
}

// MARK: - keyboardHeight
extension Publishers {
    static var keyboardHeight: AnyPublisher<CGFloat, Never> {
        let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
            .map { ($0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0 }

        let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
            .map { _ in CGFloat(0) }

        return MergeMany(willShow, willHide)
            .eraseToAnyPublisher()
    }
}

struct ResponderView_Previews: PreviewProvider {
    static var previews: some View {
        ResponderView<UITextField>.init(isFirstResponder: .constant(false)) {
            $0.placeholder = "Placeholder"
        }.previewLayout(.fixed(width: 300, height: 40))
    }
}

ResponderTextField - Il s'agit d'une enveloppe pratique de champ de texte autour de ResponderView.

struct ResponderTextField: View {
    var placeholder: String
    @Binding var text: String
    @Binding var isFirstResponder: Bool
    private var textFieldDelegate: TextFieldDelegate

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

    var body: some View {
        ResponderView<UITextField>(isFirstResponder: $isFirstResponder) {
            $0.text = self.text
            $0.placeholder = self.placeholder
            $0.delegate = self.textFieldDelegate
        }
    }
}

// MARK: - TextFieldDelegate
private extension ResponderTextField {
    final class TextFieldDelegate: NSObject, UITextFieldDelegate {
        @Binding private(set) var text: String

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

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

struct ResponderTextField_Previews: PreviewProvider {
    static var previews: some View {
        ResponderTextField("Placeholder",
                           text: .constant(""),
                           isFirstResponder: .constant(false))
            .previewLayout(.fixed(width: 300, height: 40))
    }
}

Et la façon de l'utiliser.

struct SomeView: View {
    @State private var login: String = ""
    @State private var password: String = ""
    @State private var isLoginFocused = false
    @State private var isPasswordFocused = false

    var body: some View {
        VStack {
            ResponderTextField("Login", text: $login, isFirstResponder: $isLoginFocused)
            ResponderTextField("Password", text: $password, isFirstResponder: $isPasswordFocused)
        }
    }
}

0voto

Dasoga Points 2809

Dans mon cas, je voulais mettre au point un champ de texte tout de suite, j'ai utilisé .onappear fonction

struct MyView: View {

    @FocusState private var isTitleTextFieldFocused: Bool

    @State private var title = ""

    var body: some View {
        VStack {
            TextField("Title", text: $title)
                .focused($isTitleTextFieldFocused)

        }
        .padding()
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.isTitleTextFieldFocused = true
            }

        }
    }
}

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