通过这个链接,我能够识别出TextField输入已经达到了x个字符。但是,我不知道如何使第一响应者跳转到StackView内的第二个TextField。
在SwiftUI中有没有解决方案?
我使用UITextField
和UIViewRepresentable
来实现这个功能。
为每个文本字段定义标签tag
,并声明一个布尔列表fieldFocus
,其数量与可用文本字段相同以便在Return键按下时跟踪要聚焦的文本字段,该列表将根据当前索引/标签确定下一个要聚焦的文本字段。
用法:
import SwiftUI
struct Sample: View {
@State var firstName: String = ""
@State var lastName: String = ""
@State var fieldFocus = [false, false]
var body: some View {
VStack {
KitTextField (
label: "First name",
text: $firstName,
focusable: $fieldFocus,
returnKeyType: .next,
tag: 0
)
.padding()
.frame(height: 48)
KitTextField (
label: "Last name",
text: $lastName,
focusable: $fieldFocus,
returnKeyType: .done,
tag: 1
)
.padding()
.frame(height: 48)
}
}
}
UIViewRepresentable
中的 UITextField
:
import SwiftUI
struct KitTextField: UIViewRepresentable {
let label: String
@Binding var text: String
var focusable: Binding<[Bool]>? = nil
var isSecureTextEntry: Binding<Bool>? = nil
var returnKeyType: UIReturnKeyType = .default
var autocapitalizationType: UITextAutocapitalizationType = .none
var keyboardType: UIKeyboardType = .default
var textContentType: UITextContentType? = nil
var tag: Int? = nil
var inputAccessoryView: UIToolbar? = nil
var onCommit: (() -> Void)? = nil
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = label
textField.returnKeyType = returnKeyType
textField.autocapitalizationType = autocapitalizationType
textField.keyboardType = keyboardType
textField.isSecureTextEntry = isSecureTextEntry?.wrappedValue ?? false
textField.textContentType = textContentType
textField.textAlignment = .left
if let tag = tag {
textField.tag = tag
}
textField.inputAccessoryView = inputAccessoryView
textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
uiView.isSecureTextEntry = isSecureTextEntry?.wrappedValue ?? false
if let focusable = focusable?.wrappedValue {
var resignResponder = true
for (index, focused) in focusable.enumerated() {
if uiView.tag == index && focused {
uiView.becomeFirstResponder()
resignResponder = false
break
}
}
if resignResponder {
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
let control: KitTextField
init(_ control: KitTextField) {
self.control = control
}
func textFieldDidBeginEditing(_ textField: UITextField) {
guard var focusable = control.focusable?.wrappedValue else { return }
for i in 0...(focusable.count - 1) {
focusable[i] = (textField.tag == i)
}
control.focusable?.wrappedValue = focusable
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard var focusable = control.focusable?.wrappedValue else {
textField.resignFirstResponder()
return true
}
for i in 0...(focusable.count - 1) {
focusable[i] = (textField.tag + 1 == i)
}
control.focusable?.wrappedValue = focusable
if textField.tag == focusable.count - 1 {
textField.resignFirstResponder()
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
control.onCommit?()
}
@objc func textFieldDidChange(_ textField: UITextField) {
control.text = textField.text ?? ""
}
}
}
UITextField
属性以自定义它或根据我的使用情况省略或公开您需要或不需要的属性。 - Philip Borbon使用示例:
@FocusState private var focusedField: Field?
enum Field: Int, Hashable {
case name
case country
case city
}
var body: some View {
TextField(text: $name)
.focused($focusedField, equals: .name)
.onSubmit { self.focusNextField($focusedField) }
// ...
代码:
extension View {
/// Focuses next field in sequence, from the given `FocusState`.
/// Requires a currently active focus state and a next field available in the sequence.
///
/// Example usage:
/// ```
/// .onSubmit { self.focusNextField($focusedField) }
/// ```
/// Given that `focusField` is an enum that represents the focusable fields. For example:
/// ```
/// @FocusState private var focusedField: Field?
/// enum Field: Int, Hashable {
/// case name
/// case country
/// case city
/// }
/// ```
func focusNextField<F: RawRepresentable>(_ field: FocusState<F?>.Binding) where F.RawValue == Int {
guard let currentValue = field.wrappedValue else { return }
let nextValue = currentValue.rawValue + 1
if let newValue = F.init(rawValue: nextValue) {
field.wrappedValue = newValue
}
}
/// Focuses previous field in sequence, from the given `FocusState`.
/// Requires a currently active focus state and a previous field available in the sequence.
///
/// Example usage:
/// ```
/// .onSubmit { self.focusNextField($focusedField) }
/// ```
/// Given that `focusField` is an enum that represents the focusable fields. For example:
/// ```
/// @FocusState private var focusedField: Field?
/// enum Field: Int, Hashable {
/// case name
/// case country
/// case city
/// }
/// ```
func focusPreviousField<F: RawRepresentable>(_ field: FocusState<F?>.Binding) where F.RawValue == Int {
guard let currentValue = field.wrappedValue else { return }
let nextValue = currentValue.rawValue - 1
if let newValue = F.init(rawValue: nextValue) {
field.wrappedValue = newValue
}
}
}
使用@FocusState
我采用了@Philip Borbon的答案,并进行了一些清理。我删除了很多自定义内容,只保留了最少量的内容,以便更容易看到所需内容。
struct CustomTextfield: UIViewRepresentable {
let label: String
@Binding var text: String
var focusable: Binding<[Bool]>? = nil
var returnKeyType: UIReturnKeyType = .default
var tag: Int? = nil
var onCommit: (() -> Void)? = nil
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.placeholder = label
textField.delegate = context.coordinator
textField.returnKeyType = returnKeyType
if let tag = tag {
textField.tag = tag
}
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if let focusable = focusable?.wrappedValue {
var resignResponder = true
for (index, focused) in focusable.enumerated() {
if uiView.tag == index && focused {
uiView.becomeFirstResponder()
resignResponder = false
break
}
}
if resignResponder {
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
let parent: CustomTextfield
init(_ parent: CustomTextfield) {
self.parent = parent
}
func textFieldDidBeginEditing(_ textField: UITextField) {
guard var focusable = parent.focusable?.wrappedValue else { return }
for i in 0...(focusable.count - 1) {
focusable[i] = (textField.tag == i)
}
parent.focusable?.wrappedValue = focusable
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard var focusable = parent.focusable?.wrappedValue else {
textField.resignFirstResponder()
return true
}
for i in 0...(focusable.count - 1) {
focusable[i] = (textField.tag + 1 == i)
}
parent.focusable?.wrappedValue = focusable
if textField.tag == focusable.count - 1 {
textField.resignFirstResponder()
}
return true
}
@objc func textFieldDidChange(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
仍然是必需的(它不是 UITextFieldDelegate
的一部分)。 - Tobi SchweigeriOS 15 中,我们现在可以使用 @FocusState
来控制哪个字段应该获得焦点。
这是一个演示:
struct ContentView: View {
@State private var street: String = ""
@State private var city: String = ""
@State private var country: String = ""
@FocusState private var focusedField: Field?
var body: some View {
NavigationView {
VStack {
TextField("Street", text: $street)
.focused($focusedField, equals: .street)
TextField("City", text: $city)
.focused($focusedField, equals: .city)
TextField("Country", text: $country)
.focused($focusedField, equals: .country)
}
.toolbar {
ToolbarItem(placement: .keyboard) {
Button(action: focusPreviousField) {
Image(systemName: "chevron.up")
}
.disabled(!canFocusPreviousField()) // remove this to loop through fields
}
ToolbarItem(placement: .keyboard) {
Button(action: focusNextField) {
Image(systemName: "chevron.down")
}
.disabled(!canFocusNextField()) // remove this to loop through fields
}
}
}
}
}
extension ContentView {
private enum Field: Int, CaseIterable {
case street, city, country
}
private func focusPreviousField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue - 1) ?? .country
}
}
private func focusNextField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue + 1) ?? .street
}
}
private func canFocusPreviousField() -> Bool {
guard let currentFocusedField = focusedField else {
return false
}
return currentFocusedField.rawValue > 0
}
private func canFocusNextField() -> Bool {
guard let currentFocusedField = focusedField else {
return false
}
return currentFocusedField.rawValue < Field.allCases.count - 1
}
}
今年,苹果推出了一个名为@FocusState
的新修饰符以及一个新的包装器,用于控制键盘和聚焦的状态(也称为firstResponder)。
以下是如何迭代文本字段的示例:
此外,您可以查看这个答案,了解如何使textField成为first responder或resign它以隐藏键盘以及有关如何将此枚举绑定到textFields的更多信息。
.toolbar
项目 - Mojtaba Hosseini.toolbar
项目 - undefined试试这个:
import SwiftUI
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 entry = ""
@State private var entry2 = ""
let characterLimit = 6
var body: some View {
VStack {
TextField("hallo", text: $entry)
.disabled(entry.count > (characterLimit - 1))
ResponderTextField(isFirstResponder: entry.count > (characterLimit - 1)) { uiView in
uiView.placeholder = "2nd textField"
}
}
}
}
@FocusState var focus: MyObject?
var body: some View {
ScrollView(.vertical) {
VStack {
Text("Header")
ForEach(self.myObjects) { obj in
Divider()
FocusField(displayObject: obj, focus: $focus, nextFocus: {
guard let index = self.myObjects.firstIndex(of: $0) else {
return
}
self.focus = myObjects.indices.contains(index + 1) ? myObjects[index + 1] : nil
})
}
Divider()
Text("Footer")
}
}
}
}
struct FocusField: View {
@State var displayObject: MyObject
@Binding var focus: FocusState<MyObject?>.Binding
var nextFocus: (MyObject) -> Void
var body: some View {
TextField("Test", text: $displayObject.value)
.focused(self.focus, equals: displayObject)
.submitLabel(.next)
.onSubmit {
self.nextFocus(displayObject)
}
}
}
我相信在iOS 15中,终于有了一个真正的SwiftUI解决方案来解决这个问题。
我 遇到过这个问题,并且写了一篇文章关于它,因为我找不到一个解决方法。
基本上,你可以创建几个东西来完成这个任务:
然后,您可以将闭包或函数引用传递给TextField Wrapper对象,以允许它从数组中更新Focused Object。我建议使用某种视图模型,例如FocusStateViewModel。您可以创建一个更复杂的解决方案来满足您的需求从这个gist中获取
或者,这里是解决方案的最小化重现:
import SwiftUI
struct MyObject: Identifiable, Equatable {
var id: String
public var value: String
init(name: String, value: String) {
self.id = name
self.value = value
}
}
struct ContentView: View {
@State var myObjects: [MyObject] = [
MyObject(name: "aa", value: "1"),
MyObject(name: "bb", value: "2"),
MyObject(name: "cc", value: "3"),
MyObject(name: "dd", value: "4")
]
@State var focus: MyObject?
var body: some View {
ScrollView(.vertical) {
VStack {
Text("Header")
ForEach(self.myObjects) { obj in
Divider()
FocusField(displayObject: obj, focus: $focus, nextFocus: {
guard let index = self.myObjects.firstIndex(of: $0) else {
return
}
self.focus = myObjects.indices.contains(index + 1) ? myObjects[index + 1] : nil
})
}
Divider()
Text("Footer")
}
}
}
}
struct FocusField: View {
@State var displayObject: MyObject
@FocusState var isFocused: Bool
@Binding var focus: MyObject?
var nextFocus: (MyObject) -> Void
var body: some View {
TextField("Test", text: $displayObject.value)
.onChange(of: focus, perform: { newValue in
self.isFocused = newValue == displayObject
})
.focused(self.$isFocused)
.submitLabel(.next)
.onSubmit {
self.nextFocus(displayObject)
}
}
}
@State private var passcode = ""
HStack {
TextField("", text: self.$passcode)
.introspectTextField { textField in
if self.passcode.count >= 1 {
textField.resignFirstResponder()
} else if self.passcode.count < 1 {
textField.becomeFirstResponder()
}
}
TextField("", text: self.$passcode)
.introspectTextField { textField in
if self.passcode.count >= 2
textField.resignFirstResponder()
} else if self.passcode.count < 2 {
textField.becomeFirstResponder()
}
}
}
我可能在尝试复制和粘贴代码时搞砸了实现,但你可以理解它的工作原理。
stackView.arrangedSubviews.firstIndex(of: view)
获取StackView中当前firstResponder的索引。然后,获取found index + 1
的arrangedSubview,并使用becomeFirstResponder()
将其设置为firstResponder。 - Jeroen@FocusState
控制哪个字段应该被聚焦 - 参见 此答案。 - pawello2222