web-dev-qa-db-ja.com

SwiftUI:TextFieldをファーストレスポンダーにする方法は?

これが私のSwiftUIコードです。

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

テキストフィールドがvisibleになると、テキストフィールドが最初のレスポンダーになります(つまり、フォーカスを受け取り、キーボードをポップアップします)。

46
Epaga

現時点では可能ではないようですが、同様のものを自分で実装できます。

カスタムテキストフィールドを作成し、値を追加して、最初のレスポンダーにすることができます。

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

}

追伸frameは、在庫のTextFieldのように動作しないため、追加で追加しました。

この優れたWWDC 19の講演でのCoordinatorsの詳細: SwiftUIの統合

Xcode 11.1でテスト済み

38
Matteo Pacini

https://github.com/timbersoftware/SwiftUI-Introspect を使用すると、次のことができます。

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

17
Joshua Kifer

ここに到達したが、@ 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 ?? ""
        }
    }
}
10
Rui Ying

単純なラッパー構造体-ネイティブのように機能します。

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にアクティビティインジケーターを追加する方法 を確認できます ここ

5

他の人が指摘しているように(例 @ 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)
  }
}

0
David Muller

選択した回答により、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に直接アクセスするので、first-responderの設定は単純なAppKitの方法なので、目に見えるトラブルの原因はありません。

動作するソースコード ここ をダウンロードできます。

0
Eonil