SwiftUI: Wie kann man TextField zum Ersthelfer machen?

77

Hier ist mein SwiftUICode:

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

Was ich möchte, ist, wenn das Textfeld sichtbar wird, damit das Textfeld zum Ersthelfer wird (dh Fokus erhalten und die Tastatur einblenden lassen).

Epaga
quelle

Antworten:

62

Es scheint im Moment nicht möglich zu sein, aber Sie können etwas Ähnliches selbst implementieren.

Sie können ein benutzerdefiniertes Textfeld erstellen und einen Wert hinzufügen, damit es zum Ersthelfer wird.

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

Hinweis: didBecomeFirstResponderWird benötigt, um sicherzustellen, dass das Textfeld nur einmal zum Ersthelfer wird, nicht bei jeder Aktualisierung durch SwiftUI!

Sie würden es so verwenden ...

struct ContentView : View {

    @State var text: String = ""

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

}

PS Ich habe ein hinzugefügt, frameda es sich nicht wie die Aktie verhält TextField, was bedeutet, dass hinter den Kulissen mehr los ist.

Mehr dazu Coordinatorsin diesem ausgezeichneten WWDC 19-Vortrag: Integration von SwiftUI

Getestet auf Xcode 11.4

Matteo Pacini
quelle
1
@MatteoPacini das scheint auf dem Mac nicht zu funktionieren, weißt du, ob es eine Lösung gibt, die funktioniert?
Kipelovets
@kipelovets funktioniert, wenn Sie zuerst auf ein Element in Ihrer aktiven Ansicht klicken, bevor Sie ein Modal mit diesem benutzerdefinierten Textfeld starten.
Sternenstaub4891
Ich bin gerade auf dieses Problem gestoßen, bei dem das Aufrufen von textfield.becomeFirstResponder()innerhalb dazu führte, override func layoutSubviewsdass meine App in eine Endlosschleife eintrat. Dies geschah, als ich versuchte, einen neuen ViewController zu verwenden, der auch eine Textansicht hatte, die versuchte, in layoutSubviews zum ersten Antwortgeber zu werden. Auf jeden Fall ein hinterhältiger Fehler, den man beachten sollte!
LyteSpeed
1
@Matteo wird das noch benötigt? Sie fragen sich, ob es SwiftUI-Unterstützung gibt, um den Fokus programmgesteuert auf ein bestimmtes TextField zu setzen?
Greg
3
@ Greg Ja, noch benötigt.
Roman
60

Mit https://github.com/timbersoftware/SwiftUI-Introspect kann man:

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

Joshua Kifer
quelle
1
Vielen dank für Deine Hilfe. Gut zu wissen, dass es auch mit SecureField () funktioniert.
Tristan Clet
2
Dieser Cocoapod hat auch bei mir funktioniert. Ich vermute, der einzige Grund, warum dies nicht die akzeptierte Antwort war, ist, dass diese Frage im Juni (WWDC19) gestellt und beantwortet wurde, lange bevor SwiftUI-Introspect erstellt wurde (Ende November 2019).
dmzza
2
Wenn das TextField Stilmodifikatoren enthält, müssen Sie häufig den Modifikator introspectTextField nach den Stilen hinzufügen, da die Reihenfolge wichtig ist.
user1909186
1
@Schnodderbalken Die Reihenfolge der Modifikatoren ist SEHR wichtig. Wenn Sie beispielsweise .padding (.all) haben, versuchen Sie, das .introspectTextField {tf in tf.becomeFirstResponder ()} vor dem .padding (.all) nach oben zu verschieben. Wenn Sie Probleme haben, lassen Sie es mich wissen.
user1909186
3
Ich konnte dies nicht für Introspect 0.0.6 oder 0.1.0 zum Laufen bringen. :(
Jeremy
20

Wenn Sie hier gelandet sind, aber mit der Antwort von @Matteo Pacini abgestürzt sind, beachten Sie bitte diese Änderung in Beta 4: Kann nicht der Eigenschaft zugewiesen werden: '$ text' ist für diesen Block unveränderlich :

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

sollte nutzen:

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

Wenn Sie das Textfeld zum Ersthelfer in a machen möchten sheet, beachten Sie bitte, dass Sie erst anrufen können, becomeFirstResponderwenn das Textfeld angezeigt wird. Mit anderen Worten, wenn das Textfeld von @Matteo Pacini direkt in den sheetInhalt eingefügt wird, kommt es zum Absturz.

uiView.window != nilFügen Sie zur Lösung des Problems eine zusätzliche Überprüfung der Sichtbarkeit des Textfelds hinzu. Konzentrieren Sie sich erst, nachdem es sich in der Ansichtshierarchie befindet:

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 ?? ""
        }
    }
}
Rui Ying
quelle
Ich steckte fest, weil ich den shouldChangeCharactersInUITextField-Delegaten verwendete und gleichzeitig den Text in aktualisierte updateUIView. Durch Ändern von textFieldDidChangeSelection, um den Text in der Bindung zu erfassen, funktioniert das benutzerdefinierte Steuerelement hervorragend! Vielen Dank.
P. Ent
1
Es stürzt tatsächlich ab, sheetwenn versucht wird, sich zu konzentrieren, bevor die Eingabe angezeigt wird. Ihre Lösung stürzt nicht ab, konzentriert sich aber auch nicht ... Zumindest in meinen Tests. Grundsätzlich wird updateUIView nur einmal aufgerufen.
NeverwinterMoon
Dies verhält sich seltsam, wenn sich die übergeordnete Ansicht des UITextfelds mitten in einer Animation befindet
Marwan Roushdy
Dies stürzt für mich ab, wenn ein anderes Textfeld den Fokus hatte, als das Blatt mit dem AutoFocusTextFieldpräsentiert wurde. Ich musste den Anruf becomeFirstResponder()in einen DispatchQueue.main.asyncBlock packen.
Brad
Das Einpacken DispatchQueue.main.asyncfunktioniert für mich mit Übergangsanimationen definitiv nicht. Wenn Sie beispielsweise NavigationView verwenden und von Ansicht 1 zu Ansicht 2 wechseln (wo comeFirstResponder aufgerufen wird), wird die Übergangsanimation mit Async zweimal abgespielt.
NeverwinterMoon
13

Einfache Wrapper-Struktur - Funktioniert wie eine native:

Beachten Sie, dass die Unterstützung für die Textbindung wie in den Kommentaren angegeben hinzugefügt wurde

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

Verwendung:

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

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

🎁 Bonus: Vollständig anpassbar

LegacyTextField(text: $text, isFirstResponder: $isFirstResponder) {
    $0.textColor = .red
    $0.tintColor = .blue
}

Diese Methode ist vollständig anpassbar. Zum Beispiel können Sie sehen , wie eine Aktivitätsanzeige in SwiftUI hinzuzufügen mit der gleichen Methode hier

Mojtaba Hosseini
quelle
1
'ResponderTextField.Type' kann nicht konvertiert werden in '(Bool, Escape (ResponderTextField.TheUIView) -> ()) -> ResponderTextField' (auch bekannt als '(Bool, Escape (UITextField) -> ()) -> ResponderTextField')
stardust4891
Woher hast du das?
Mojtaba Hosseini
4
Hier fehlt die $ text-Bindung (und Beschriftung), die TextField normalerweise hat? (Ich konnte es sowieso nicht zum
Laufen bringen
Wo ist das Textfeld?
Peter Schorn
1
@drtofu Ich habe verbindliche Textunterstützung hinzugefügt.
Mojtaba Hosseini
6

Wie andere angemerkt haben (z. B. Kommentar von @kipelovets zur akzeptierten Antwort , z. B. Antwort von @ Eonil ), habe ich auch keine der akzeptierten Lösungen für MacOS gefunden. Ich hatte jedoch etwas Glück, als ich NSViewControllerRepresentable verwendet habe , um ein NSSearchField als Ersthelfer in einer SwiftUI-Ansicht anzuzeigen:

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

Beispiel für eine SwiftUI-Ansicht:

struct ContentView: View {

  @State private var text: String = ""

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

David Muller
quelle
Das ist großartig - fast genau das, wonach ich gesucht habe. Jetzt muss ich nur noch herausfinden, wie ich es immer als Ersthelfer haben kann.
Bouwerp
Hat sehr gut funktioniert. Vielen Dank!
Simon
3

Um diese fehlende Funktionalität zu ergänzen, können Sie SwiftUIX mit Swift Package Manager installieren:

  1. Öffnen Sie in Xcode Ihr Projekt und navigieren Sie zu Datei → Schnelle Pakete → Paketabhängigkeit hinzufügen ...
  2. Fügen Sie die Repository-URL ( https://github.com/SwiftUIX/SwiftUIX ) ein und klicken Sie auf Weiter.
  3. Wählen Sie unter Regeln Zweig aus (Zweig auf Master eingestellt).
  4. Klicken Sie auf Fertig stellen.
  5. Öffnen Sie die Projekteinstellungen, fügen Sie SwiftUI.framework zu den verknüpften Frameworks und Bibliotheken hinzu und setzen Sie den Status auf Optional.

Weitere Informationen: https://github.com/SwiftUIX/SwiftUIX

import SwiftUI
import SwiftUIX

struct ContentView : View {

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

    var body: some View {
        return VStack {
            if showingTextField {
                CocoaTextField("Placeholder text", text: $text)
                    .isFirstResponder(true)
                    .frame(width: 300, height: 48, alignment: .center)
            }
            Button(action: { self.showingTextField.toggle() }) {
                Text ("Show")
            }
        }
    }
}
Joanna J.
quelle
2

📦 ResponderChain

Ich habe dieses kleine Paket für die plattformübergreifende Ersthelferbehandlung erstellt, ohne Ansichten in Unterklassen einzuteilen oder benutzerdefinierte ViewRepresentables in SwiftUI zu erstellen

https://github.com/Amzd/ResponderChain

So wenden Sie es auf Ihr Problem an

SceneDelegate.swift

...
// Set the ResponderChain as environmentObject
let rootView = ContentView().environmentObject(ResponderChain(forWindow: window))
...

ContentView.swift

struct ContentView: View {

    @EnvironmentObject var chain: ResponderChain
    @State var showingTextField = false
    @State var text = ""

    var body: some View {
        return VStack {
            if showingTextField {
                TextField($text).responderTag("field1").onAppear {
                    DispatchQueue.main.async {
                        chain.firstResponder = "field1"
                    }
                }
            }
            Button(action: { self.showingTextField.toggle() }) {
                Text ("Show")
            }
        }
    }
}
Casper Zandbergen
quelle
Die eleganteste Lösung, die ich bisher gesehen habe. Großartige Unterstützung für SwiftUI. Ich habe mich selbst benutzt und kann es nur empfehlen!
Legolas Wang
1

Die ausgewählte Antwort verursacht ein Endlosschleifenproblem mit AppKit. Ich weiß nichts über den UIKit-Fall.

Um dieses Problem zu vermeiden, empfehle ich, die NSTextFieldInstanz direkt freizugeben.

import AppKit
import SwiftUI

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

Sie können das so verwenden.

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()

Dies bricht die reine Wertesemantik, aber abhängig von AppKit bedeutet dies, dass Sie die reine Wertesemantik teilweise aufgeben und sich etwas Schmutz leisten. Dies ist ein magisches Loch, das wir derzeit brauchen, um den Mangel an Ersthelfer-Kontrolle in SwiftUI zu beheben.

Da wir NSTextFielddirekt darauf zugreifen , ist die Einstellung des Ersthelfers eine einfache AppKit-Methode, daher keine sichtbare Problemquelle.

Sie können den funktionierenden Quellcode hier herunterladen .

eonil
quelle
0

Da die Responder-Kette nicht für die Verwendung über SwiftUI verfügbar ist, müssen wir sie mit UIViewRepresentable verwenden.

Schauen Sie sich den folgenden Link an, da ich eine Problemumgehung erstellt habe, die ähnlich wie bei UIKit funktioniert.

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

Anshuman Singh
quelle
0

Es ist meine Variante der Implementierung, die auf den Lösungen @Mojtaba Hosseini und @Matteo Pacini basiert. Ich bin noch neu in SwiftUI, daher kann ich die absolute Richtigkeit des Codes nicht garantieren, aber es funktioniert.

Ich hoffe es wäre für jemanden hilfreich.

ResponderView: Dies ist eine generische Responder-Ansicht, die mit jeder UIKit-Ansicht verwendet werden kann.

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 - Dies ist ein praktischer Textfeld-Wrapper um 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))
    }
}

Und wie man das nutzt.

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)
        }
    }
}
Dmytro Shvetsov
quelle
1
Kann dies für die Verwendung mit macOS geändert werden?
Peter Schorn
Ich werde es versuchen :) In der Zwischenzeit kann Ihnen dieses forums.developer.apple.com/thread/125339 helfen.
Dmytro Shvetsov
0

Dies ist ein ViewModifier, der mit Introspect arbeitet . Es funktioniert für AppKit MacOS, Xcode 11.5

    struct SetFirstResponderTextField: ViewModifier {
      @State var isFirstResponderSet = false

      func body(content: Content) -> some View {
         content
            .introspectTextField { textField in
            if self.isFirstResponderSet == false {
               textField.becomeFirstResponder()
               self.isFirstResponderSet = true
            }
         }
      }
   }
Gregor Brandt
quelle
0

Wir haben eine Lösung, mit der Sie den Ersthelfer mühelos steuern können.

https://github.com/mobilinked/MbSwiftUIFirstResponder

TextField("Name", text: $name)
    .firstResponder(id: FirstResponders.name, firstResponder: $firstResponder, resignableUserOperations: .all)

TextEditor(text: $notes)
    .firstResponder(id: FirstResponders.notes, firstResponder: $firstResponder, resignableUserOperations: .all)
SWIFT-Code
quelle
3
Bitte erläutern Sie, was Ihr Code tut und wie er es tut. Postleitzahl niemals alleine.
M-Chen-3
-2

Da SwiftUI 2 noch keine Ersthelfer unterstützt, verwende ich diese Lösung. Es ist schmutzig, kann aber in einigen Anwendungsfällen funktionieren, wenn Sie nur 1 UITextFieldund 1 haben UIWindow.

import SwiftUI

struct MyView: View {
    @Binding var text: String

    var body: some View {
        TextField("Hello world", text: $text)
            .onAppear {
                UIApplication.shared.windows.first?.rootViewController?.view.textField?.becomeFirstResponder()
            }
    }
}

private extension UIView {
    var textField: UITextField? {
        subviews.compactMap { $0 as? UITextField }.first ??
            subviews.compactMap { $0.textField }.first
    }
}
Opel
quelle