Как заставить 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)
                }
            }
        }
    }
}

https://developer.apple.com

Для тех, кто оказался здесь, но столкнулся с аварией, используя ответ @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:

  1. В Xcode откройте свой проект и перейдите к File → Swift Packages → Add Package Dependency...
  2. Вставьте URL-адрес репозитория (https://github.com/SwiftUIX/SwiftUIX) и нажмите Далее.
  3. В поле "Правила" выберите "Ветвь" (с ветвью "ведущая").
  4. Щелкните Готово.
  5. Откройте настройки проекта, добавьте 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 также предлагает использовать клавиатуру для перемещения первого респондента вниз по цепочке, что очень удобно.

Другие вопросы по тегам