skip to Main Content

I have list of custom fields in a custom form view. The form view is loaded in the content view, which will inform the form view to move to the next field, when the user taps on "Next" button.

The question is how the form view will make the next custom field focused, when the moveToNextField() function called?

This is how my form looks like

enter image description here

Here’s the code for the custom field

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

Here’s the code for the form view

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

The code in the Content view

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

2

Answers


  1. Chosen as BEST ANSWER

    I found my issue, I was using @State to track the current focused field, which is getting reset every time the view changes.

    So I had to us @ObservableObject with @Published property.

    Here is my final working code.

    Custom field

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

    Form view

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

    Content view

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

  2. Declare a FocusState var for each field. Then turn on the state of the field where you want to move the focus.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search