如何移动到 SwiftUI 中的下一个 TextField?
How to move to next TextField in SwiftUI?
使用 Swift5.1.2,iOS13.2,Xcode-11.2,
Stackview 中有多个 TextField,我想在用户在第一个 TextField 中键入 x 数量的字符后立即移至下一个 TextField。
使用 ,我实现了识别 TextField 条目何时达到 x 字符数。但是,我不知道如何让 firstResponder 跳转到我的 StackView 中的第二个 TextField。
SwiftUI 有解决这个问题的方法吗?
试试这个:
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"
}
}
}
}
我能够通过 Introspect 库完成这项工作。 https://github.com/siteline/SwiftUI-Introspect:
@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()
}
}
}
我可能因为尝试复制和粘贴我的代码而搞砸了实现,但您了解了它的工作原理。
我正在使用 UITextField
和 UIViewRepresentable
来实现这一点。
定义每个文本字段的 tag
并声明一个布尔值列表,该列表具有相同数量的可用文本字段以聚焦 return 键 fieldFocus
,这将跟踪根据当前 index/tag.
接下来要关注哪个文本字段
用法:
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)
}
}
}
UITextField
在 UIViewRepresentable
:
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 ?? ""
}
}
}
iOS 15
今年,Apple 引入了一个新修饰符以及一个名为 @FocusState
的新包装器,用于控制键盘和聚焦键盘的状态('aka' firstResponder)。
以下是如何迭代文本字段的示例:
此外,您可以查看 以了解如何创建文本字段第一响应程序或退出它以隐藏键盘并了解有关如何操作的更多信息将此枚举绑定到文本字段。
iOS 15
在 iOS 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: [=11=].rawValue - 1) ?? .country
}
}
private func focusNextField() {
focusedField = focusedField.map {
Field(rawValue: [=11=].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
}
}
注意:截至 Xcode 13 beta 1 @FocusState
在 Form
/List
中不起作用。这应该会在下一个版本中修复。
iOS 15+
使用@FocusState
之前 iOS 15
我已经接受了@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 ?? ""
}
}
}
如果您可以使用 iOS 15,Mojtaba 提出的解决方案非常好。由于大多数项目必须支持旧版本的 iOS,因此它不起作用。但是,如果您使用的是 iOS 13 或 iOS 14,则可以使用提供该功能的 Focuser 库。
您可以从 Github 下载示例项目以查看示例。然而 API 被建模为在 iOS 15.
中工作
我终于相信 iOS 15 有一个真正的 SwiftUI 解决方案可以解决这个问题。
我 , and wrote and article about it 因为找不到。
基本上你可以创建一些东西来完成这个:
- 焦点对象:观察到的可识别对象或焦点数组索引 int 变量
- 可聚焦对象数组:与您要作为第一响应者迭代的文本字段关联的可识别对象数组
- TextFieldWrapper:用于管理每个 TextField 的 FocusState 和更新 Focus 对象的对象(参见第一个项目符号)
然后将闭包或函数引用传递给 TextField Wrapper 对象,以允许它更新数组中的焦点对象。我会使用某种视图模型,也许是 FocusStateViewModel。您可以制定更复杂的解决方案来满足您的需求 from this 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: [=10=]) 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)
}
}
}
使用 iOS 15+ @FocusState - 通用解决方案
用法示例:
@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
}
}
}
使用 Swift5.1.2,iOS13.2,Xcode-11.2,
Stackview 中有多个 TextField,我想在用户在第一个 TextField 中键入 x 数量的字符后立即移至下一个 TextField。
使用
SwiftUI 有解决这个问题的方法吗?
试试这个:
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"
}
}
}
}
我能够通过 Introspect 库完成这项工作。 https://github.com/siteline/SwiftUI-Introspect:
@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()
}
}
}
我可能因为尝试复制和粘贴我的代码而搞砸了实现,但您了解了它的工作原理。
我正在使用 UITextField
和 UIViewRepresentable
来实现这一点。
定义每个文本字段的 tag
并声明一个布尔值列表,该列表具有相同数量的可用文本字段以聚焦 return 键 fieldFocus
,这将跟踪根据当前 index/tag.
用法:
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)
}
}
}
UITextField
在 UIViewRepresentable
:
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 ?? ""
}
}
}
iOS 15
今年,Apple 引入了一个新修饰符以及一个名为 @FocusState
的新包装器,用于控制键盘和聚焦键盘的状态('aka' firstResponder)。
以下是如何迭代文本字段的示例:
此外,您可以查看
iOS 15
在 iOS 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: [=11=].rawValue - 1) ?? .country
}
}
private func focusNextField() {
focusedField = focusedField.map {
Field(rawValue: [=11=].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
}
}
注意:截至 Xcode 13 beta 1 @FocusState
在 Form
/List
中不起作用。这应该会在下一个版本中修复。
iOS 15+
使用@FocusState
之前 iOS 15
我已经接受了@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 ?? ""
}
}
}
如果您可以使用 iOS 15,Mojtaba 提出的解决方案非常好。由于大多数项目必须支持旧版本的 iOS,因此它不起作用。但是,如果您使用的是 iOS 13 或 iOS 14,则可以使用提供该功能的 Focuser 库。
您可以从 Github 下载示例项目以查看示例。然而 API 被建模为在 iOS 15.
中工作我终于相信 iOS 15 有一个真正的 SwiftUI 解决方案可以解决这个问题。
我
基本上你可以创建一些东西来完成这个:
- 焦点对象:观察到的可识别对象或焦点数组索引 int 变量
- 可聚焦对象数组:与您要作为第一响应者迭代的文本字段关联的可识别对象数组
- TextFieldWrapper:用于管理每个 TextField 的 FocusState 和更新 Focus 对象的对象(参见第一个项目符号)
然后将闭包或函数引用传递给 TextField Wrapper 对象,以允许它更新数组中的焦点对象。我会使用某种视图模型,也许是 FocusStateViewModel。您可以制定更复杂的解决方案来满足您的需求 from this 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: [=10=]) 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)
}
}
}
使用 iOS 15+ @FocusState - 通用解决方案
用法示例:
@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
}
}
}