如何在 SwiftUI 中使用带有自定义字段的嵌套自定义表单以编程方式移动到下一个字段

How to move to next field programmatically in nested custom form with custom fields in SwiftUI

我在自定义表单视图中有自定义字段列表。表单视图加载到内容视图中,当用户点击“下一步”按钮时,内容视图将通知表单视图移动到下一个字段。

问题是当调用 moveToNextField() 函数时,表单视图如何使下一个自定义字段成为焦点?

这是我的表格的样子

这是自定义字段的代码

enum InputFieldType {
    case text, number, dropdown
}

struct CustomField: View {
    let tag: Int
    let type: InputFieldType
    let title: String
    var dropdownItems: Array<String> = []
    var placeholder: String = ""
    @State var text: String = ""
    @State var enabled: Bool = true

    @FocusState private var focusField: Bool
    private let dropdownImage = Image(systemName: "chevron.down")
    @State private var showDropdown: Bool = false

    var body: some View {
        VStack(alignment: .leading, spacing: 8.0) {
            Text(title)
                .foregroundColor(.gray)
                .frame(alignment: .leading)
            ZStack(alignment: .leading) {
                // The placeholder view
                Text(placeholder).foregroundColor( enabled ? .gray.opacity(0.3) : .gray.opacity(0.5))
                    .opacity(text.isEmpty ? 1 : 0)
                    .padding(.horizontal, 8)
                // Text field
                TextField("", text: $text)
                    .disabled(!enabled)
                    .frame(height: 44)
                    .textInputAutocapitalization(.sentences)
                    .foregroundColor(.white)
                    .padding(.horizontal, 8)
                    .keyboardType( type == .number ? .decimalPad : .default)
                    .focused($focusField)

            }.background(Color.red.opacity(0.1))
                .cornerRadius(5)
        }
    }
}

这是表单视图的代码

struct FormView: View {

    func moveToNextField() -> Bool {
        return false
    }

    var body: some View {
        VStack {
            ScrollView(.vertical) {
                VStack(spacing: 24) {
                    CustomField(tag: 0, type: .text, title: "First name", placeholder: "John", text: "", enabled: false)
                    CustomField(tag: 1, type: .text, title: "Surname", placeholder: "Mike", text: "")
                    CustomField(tag: 2, type: .text, title: "Gender (Optional)", placeholder: "Optional", text: "")
                    CustomField(tag: 3, type: .dropdown, title: "Body type", dropdownItems: ["1", "2", "3"], placeholder: "Skinny", text: "")
                    CustomField(tag: 4, type: .number, title: "Year of birth", placeholder: "2000", text: "")
                    Spacer()
                }
            }
        }.onTapGesture {

        }
        .background(Color.clear)
        .padding(.horizontal, 16)
    }
}

内容视图中的代码

struct ContentView: View {

    let formView = FormView()
    var body: some View {
        VStack {
            Spacer(minLength: 30)
            formView
                .padding(.vertical)
            Button("Next") {
                if formView.moveToNextField()  {
                    return
                }
                // validate the form
            }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .center)
                .background(Color.secondary)
                .cornerRadius(5)
                .padding(.horizontal, 16)
            Spacer(minLength: 20)
        }.background(Color.primary)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().preferredColorScheme(.dark)
    }
}

为每个字段声明一个 FocusState 变量。然后开启要移动焦点的字段状态。

我发现了我的问题,我正在使用 @State 来跟踪当前的焦点字段,每次视图更改时都会重置它。

所以我不得不 @ObservableObject@Published 属性。

这是我最终的工作代码。

自定义字段

enum InputFieldType {
    case text, number, dropdown
}

struct CustomField: View {
    let tag: Int
    let type: InputFieldType
    let title: String
    var dropdownItems: Array<String> = []
    var placeholder: String = ""
    @State var text: String = ""
    @State var enabled: Bool = true
    @Binding var focusTag: Int

    @FocusState private var focusField: Bool
    private let dropdownImage = Image(systemName: "chevron.down")
    @State private var showDropdown: Bool = false

    var body: some View {
        VStack(alignment: .leading, spacing: 8.0) {
            Text(title)
                .foregroundColor(.gray)
                .frame(alignment: .leading)
            ZStack(alignment: .leading) {
                // The placeholder view
                Text(placeholder).foregroundColor( enabled ? .gray.opacity(0.3) : .gray.opacity(0.5))
                    .opacity(text.isEmpty ? 1 : 0)
                    .padding(.horizontal, 8)
                // Text field
                TextField("", text: $text)
                    .disabled(!enabled)
                    .frame(height: 44)
                    .textInputAutocapitalization(.sentences)
                    .foregroundColor(.white)
                    .padding(.horizontal, 8)
                    .keyboardType( type == .number ? .decimalPad : .default)
                    .onChange(of: focusTag, perform: { newValue in
                        focusField = newValue == tag
                    })
                    .focused($focusField)
                    .onChange(of: focusField, perform: { newValue in
                        if type != .dropdown, newValue, focusTag != tag {
                            focusTag = tag
                        }
                    })

            }.background(Color.red.opacity(0.1))
                .cornerRadius(5)
        }
    }
}

窗体视图

fileprivate class FocusStateObserver: ObservableObject {
    @Published var focusFieldTag: Int = -1
}

struct FormView: View {

    @ObservedObject private var focusStateObserver = FocusStateObserver()

    func moveToNextField() -> Bool {
        if focusStateObserver.focusFieldTag < 4 {
            switch focusStateObserver.focusFieldTag {
            case 0:
                focusStateObserver.focusFieldTag = 1
            case 1:
                focusStateObserver.focusFieldTag = 2
            case 2:
                focusStateObserver.focusFieldTag = 4
            default:
                break
            }

            return true
        }

        return false
    }

    var body: some View {
        VStack {
            ScrollView(.vertical) {
                VStack(spacing: 24) {
                    CustomField(tag: 0, type: .text, title: "First name", placeholder: "John", text: "", enabled: false, focusTag: $focusStateObserver.focusFieldTag)
                    CustomField(tag: 1, type: .text, title: "Surname", placeholder: "Mike", text: "", focusTag: $focusStateObserver.focusFieldTag)
                    CustomField(tag: 2, type: .text, title: "Gender (Optional)", placeholder: "Optional", text: "", focusTag: $focusStateObserver.focusFieldTag)
                    CustomField(tag: 3, type: .dropdown, title: "Body type", dropdownItems: ["1", "2", "3"], placeholder: "Skinny", text: "", focusTag: $focusStateObserver.focusFieldTag)
                    CustomField(tag: 4, type: .number, title: "Year of birth", placeholder: "2000", text: "", focusTag: $focusStateObserver.focusFieldTag)
                    Spacer()
                }
            }
        }.onTapGesture {
            endEditing()
        }
        .background(Color.clear)
        .padding(.horizontal, 16)
    }
}

extension View {
    func endEditing() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

内容查看

struct ContentView: View {

    let formView = FormView()
    var body: some View {
        VStack {
            Spacer(minLength: 30)
            formView
                .padding(.vertical)
            Button("Next") {
                if formView.moveToNextField()  {
                    return
                }
                endEditing()
            }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .center)
                .background(Color.secondary)
                .cornerRadius(5)
                .padding(.horizontal, 16)
            Spacer(minLength: 20)
        }.background(Color.primary)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().preferredColorScheme(.dark)
    }
}