Как заставить TextField стать первым респондентом?
Вот мой код...
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")
}
}
}
}
То, что я хочу, - это когда текстовое поле становится видимым, чтобы текстовое поле стало первым респондентом (то есть получил фокус и поднял клавиатуру).
23 ответа
На данный момент это не представляется возможным, но вы можете реализовать нечто подобное самостоятельно.
Вы можете создать собственное текстовое поле и добавить значение, чтобы оно стало первым респондентом.
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
}
}
}
Примечание: didBecomeFirstResponder
необходимо, чтобы текстовое поле становилось первым респондентом только один раз, а не при каждом обновлении SwiftUI
!
Вы бы использовали это так...
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 я добавил frame
поскольку это не ведет себя как запас TextField
Это означает, что за кулисами происходит больше всего.
Еще Coordinators
в этом превосходном выступлении на WWDC 19: интеграция SwiftUI
Используя https://github.com/timbersoftware/SwiftUI-Introspect, можно:
TextField("", text: $value)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
Простая структура-оболочка - работает как нативная:
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)
}
}
Применение:
struct ContentView: View {
@State private var isActive = false
var body: some View {
ResponderTextField(isFirstResponder: isActive) // Just set `isActive` anyway you like
}
}
Бонус: полностью настраиваемый
ResponderTextField(isFirstResponder: isActive) {
$0.textColor = .red
$0.tintColor = .blue
}
Этот метод полностью адаптируем. Например, вы можете увидеть, как добавить индикатор активности в SwiftUI тем же методом здесь.
iOS 15.0+
macOS 12.0+,Mac Catalyst 15.0+,tvOS 15.0+,watchOS 8.0+
сфокусированный (_: равно :)
Изменяет это представление, привязывая его состояние фокуса к заданному значению состояния.
struct LoginForm {
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)
}
}
}
}
}
Для тех, кто оказался здесь, но столкнулся с аварией, используя ответ @Matteo Pacini, учтите это изменение в бета-версии 4: Невозможно присвоить свойству: '$text' является неизменным относительно этого блока:
init(text: Binding<String>) {
$text = text
}
следует использовать:
init(text: Binding<String>) {
_text = text
}
И если вы хотите, чтобы текстовое поле стало первым респондентом в sheet
пожалуйста, имейте в виду, что вы не можете позвонить becomeFirstResponder
пока текстовое поле не отображается. Другими словами, поместив текстовое поле @Matteo Pacini непосредственно в sheet
содержание вызывает сбой.
Чтобы решить проблему, добавьте дополнительную проверку uiView.window != nil
для видимости текстового поля. Фокус только после того, как это в иерархии представления:
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 ?? ""
}
}
}
📦 ResponderChain
Я сделал этот небольшой пакет для кроссплатформенной обработки первого респондента без создания подклассов представлений или создания пользовательских ViewRepresentables в SwiftUI.
https://github.com/Amzd/ResponderChain
Как применить это к вашей проблеме
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")
}
}
}
}
В моем случае я хотел сразу сфокусировать текстовое поле, я использовал
.onappear
функция
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
}
}
}
}
Как отмечали другие (например, комментарий @kipelovets к принятому ответу, например , ответ @Eonil), я также не нашел ни одного из принятых решений для работы в macOS. Однако мне повезло, я использовал NSViewControllerRepresentable, чтобы NSSearchField отображался в качестве первого респондента в представлении 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>
) {
}
}
Пример представления SwiftUI:
struct ContentView: View {
@State private var text: String = ""
var body: some View {
FirstResponderNSSearchFieldRepresentable(text: $text)
}
}
SwiftUI
struct ContentView: View {
enum Field {
case firstName
case lastName
case emailAddress
}
@State private var firstName = ""
@State private var lastName = ""
@State private var emailAddress = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("Enter your first name", text: $firstName)
.focused($focusedField, equals: .firstName)
.submitLabel(.next)
TextField("Enter your last name", text: $lastName)
.focused($focusedField, equals: .lastName)
.submitLabel(.next)
TextField("Enter your email address", text: $emailAddress)
.focused($focusedField, equals: .emailAddress)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstName:
focusedField = .lastName
case .lastName:
focusedField = .emailAddress
default:
focusedField = nil
}
}
}
}
Описание: добавьте перечисление с вариантами текстовых полей и свойство, обернутое в
@FocusState
с типом этого перечисления. Добавлять
focused(_:equals:)
модификатор, чтобы иметь значение привязки, равное случаям перечисления. Теперь вы можете изменить
focusedField
в любое текстовое поле, на которое вы хотите навести курсор, или откажитесь от первого ответчика, назначив nil для focusField.
Чтобы восполнить эту недостающую функциональность, вы можете установить SwiftUIX с помощью Swift Package Manager:
- В Xcode откройте свой проект и перейдите к File → Swift Packages → Add Package Dependency...
- Вставьте URL-адрес репозитория (https://github.com/SwiftUIX/SwiftUIX) и нажмите Далее.
- В поле "Правила" выберите "Ветвь" (с ветвью "ведущая").
- Щелкните Готово.
- Откройте настройки проекта, добавьте SwiftUI.framework в связанные платформы и библиотеки, установите для параметра Статус значение Необязательно.
Дополнительная информация: 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")
}
}
}
}
На самом деле это не ответ, просто на основе отличного решения Casper с удобным модификатором -
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))
}
}
Просто используйте как -
TextField("Enter value:", text: $quantity)
.startInput()
У нас есть решение, которое упрощает управление первым респондентом.
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)
Я использовал немного другой подход - вместо
UIViewRepresentable
на основе
UITextField
я сделал его на основе и подключил его в иерархию представлений SwiftUI с
background
модификатор. Внутри
UIView
Я добавил логику, чтобы найти первое представление, которое
canBecomeFirstResponder
в подпредставлениях и родительских представлениях.
private struct FocusableUIView: UIViewRepresentable {
var isFirstResponder: Bool = false
class Coordinator: NSObject {
var didBecomeFirstResponder = false
}
func makeUIView(context: UIViewRepresentableContext<FocusableUIView>) -> UIView {
let view = UIView()
view.backgroundColor = .clear
return view
}
func makeCoordinator() -> FocusableUIView.Coordinator {
return Coordinator()
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<FocusableUIView>) {
guard uiView.window != nil, isFirstResponder, !context.coordinator.didBecomeFirstResponder else {
return
}
var foundRepsonder: UIView?
var currentSuperview: UIView? = uiView
repeat {
foundRepsonder = currentSuperview?.subviewFirstPossibleResponder
currentSuperview = currentSuperview?.superview
} while foundRepsonder == nil && currentSuperview != nil
guard let responder = foundRepsonder else {
return
}
DispatchQueue.main.async {
responder.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
private extension UIView {
var subviewFirstPossibleResponder: UIView? {
guard !canBecomeFirstResponder else { return self }
for subview in subviews {
if let firstResponder = subview.subviewFirstPossibleResponder {
return firstResponder
}
}
return nil
}
}
Вот пример того, как использовать его, чтобы сделать TextField автофокусом (+ бонус использовать
@FocusState
новый iOS 15 api).
extension View {
@ViewBuilder
func autofocus() -> some View {
if #available(iOS 15, *) {
modifier(AutofocusedViewModifiers.Modern())
} else {
modifier(AutofocusedViewModifiers.Legacy())
}
}
}
private enum AutofocusedViewModifiers {
struct Legacy: ViewModifier {
func body(content: Content) -> some View {
content
.background(FocusableUIView(isFirstResponder: isFocused))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
}
}
@State private var isFocused = false
}
@available(iOS 15, *)
struct Modern: ViewModifier {
func body(content: Content) -> some View {
content
.focused($isFocused)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
}
}
@FocusState private var isFocused: Bool
}
}
Пример просмотра содержимого:
struct ContentView: View {
@State private var text = ""
var body: some View {
VStack {
TextField("placeholder", text: $text)
Text("some text")
}
.autofocus()
}
}
Это ViewModifier, который работает с самоанализом. Работает для 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
}
}
}
}
Выбранный ответ вызывает проблему с бесконечным циклом в AppKit. Не знаю, как обстоят дела с UIKit.
Чтобы избежать этой проблемы, я рекомендую просто поделиться NSTextField
экземпляр напрямую.
import AppKit
import SwiftUI
struct Sample1: NSViewRepresentable {
var textField: NSTextField
func makeNSView(context:NSViewRepresentableContext<Sample1>) -> NSView { textField }
func updateNSView(_ x:NSView, context:NSViewRepresentableContext<Sample1>) {}
}
Вы можете использовать это вот так.
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()
Это нарушает семантику чистого значения, но в зависимости от AppKit вы частично отказываетесь от семантики чистого значения и допускаете некоторую грязь. Это волшебная дыра, которая нам нужна прямо сейчас, чтобы справиться с отсутствием управления оперативным реагированием в SwiftUI.
Когда мы получаем доступ NSTextField
напрямую, установка первого респондента осуществляется простым способом AppKit, поэтому нет видимого источника проблем.
Вы можете скачать рабочий исходный код здесь.
В простейшей форме в iOS 13 (без использования сторонних sdk / repo или если вы не обновились до iOS 14, чтобы использовать
focus
модификаторы)
struct CustomTextField: UIViewRepresentable {
func makeUIView(context: Context) -> UITextField {
UITextField(frame: .zero)
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.becomeFirstResponder()
}
}
Использование:
struct ContentView : View {
var body: some View {
CustomTextField()
.frame(width: 300, height: 50)
.background(Color.red)
}
}
Поскольку Responder Chain недоступен для использования через SwiftUI, поэтому мы должны использовать его, используя UIViewRepresentable.
Взгляните на приведенную ниже ссылку, так как я сделал обходной путь, который может работать аналогично тому, как мы используем UIKit.
/questions/51341883/programmnaya-avtofokusirovka-textfield-v-swiftui/51341891#51341891
Расширяя ответ @JoshuaKifer выше, если вы имеете дело с ошибкой анимации навигации при использовании Introspect для создания первого респондента текстового поля. Использовать этот:
import SchafKit
@State var field: UITextField?
TextField("", text: $value)
.introspectTextField { textField in
field = textField
}
.onDidAppear {
field?.becomeFirstResponder()
}
Подробнее об этом решении здесь: https://githubmemory.com/repo/siteline/SwiftUI-Introspect/issues/102
Я знаю, что уже слишком поздно, но если это кому-то поможет, я так и сделаю.
import SwiftUI
import Introspect
struct MyView: View {
@Binding var text1: String
@Binding var text2: String
@State private var toggleTF: Bool = false
var body: some View {
TextField("TextField 1", text: $text1)
.introspectTextField{ tF in
if toggleTF {
tF.becomeFirstResponder()
}
}
TextField("TextField 2", text: $text1)
.introspectTextField{ tF in
if !toggleTF {
tF.becomeFirstResponder()
}
}
Button("Toggle textfields") {
toggleTF.toggle()
}
}
}
Если у вас возникли проблемы с ответом @JoshuaKifer или @ahaze , я решил свою проблему, используя модификатор в родительском классе , а не в самом TextField.
Что я делал:
TextField("Placeholder text...", text: $searchInput)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
Как я решил свою проблему:
YourParentStruct(searchInput: $searchInput)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
Я помещу определение родительской структуры ниже для наглядности.
struct YourParentStruct: View {
@Binding var searchInput: String
var body: some View {
HStack {
TextField("Placeholder text...", text: $searchInput)
.padding()
.background(Color.gray)
.cornerRadius(10)
}
}
}
Это мой вариант реализации, основанный на решениях @Mojtaba Hosseini и @Matteo Pacini. Я все еще новичок в SwiftUI, поэтому не могу гарантировать абсолютную правильность кода, но он работает.
Надеюсь, это будет кому-то полезно.
ResponderView: это представление универсального респондента, которое можно использовать с любым представлением 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 - это удобная оболочка текстового поля для 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))
}
}
И способ использовать это.
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)
}
}
}
Поскольку SwiftUI 2 не поддерживает службы быстрого реагирования, я использую это решение. Он грязный, но может работать в некоторых случаях, когда у вас только 1
UITextField
и 1
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
}
}
Правильный способ SwiftUI - использовать
@FocusState
как уже упоминалось выше. Однако этот API доступен только для iOS 15. Если вы используете iOS 14 или iOS 13, вы можете использовать библиотеку Focuser, которая смоделирована в соответствии с Apple API.
https://github.com/art-technologies/swift-focuser
Вот пример кода. Вы заметите, что API выглядит почти так же, как Apple, однако Focuser также предлагает использовать клавиатуру для перемещения первого респондента вниз по цепочке, что очень удобно.