.focused does not work if TextField is obscured by they iOS keyboard

488 Views Asked by At

EDIT:

After running @Yrb's code below, I realized the ForEach loop was a red herring. The actual issue here is that .focused will simply close the keyboard if you attempt to focus a field that is obscured by the keyboard. Reproducible with the following code:

struct TestFields: View {
    @State private var testField: String = ""
    @FocusState private var focusedField: Int?
    
    var body: some View {
        Form {
            ForEach(0..<20) { i in
                TextField("test", text: $testField)
                    .focused($focusedField, equals: i)
            }
        }
        .onSubmit {
            if let current = focusedField {
                focusedField = current + 1
            }
        }
    }
}

Running the above you will see normal behavior until your device attempts to focus a field you cannot see.

Not sure if I should close this and open a new question with the proper title.

ORIGINAL QUESTION:

I have run across some strange behavior when using .focused for a dynamic number of textfields:

struct TestView: View {
    @State private var options: [String] = Array(repeating: "", count: 7)
    @FocusState private var focusedField: Int?
    
    var body: some View {
        Form {
            ForEach($options.indices) { i in
                TextField("Option \(i + 1)", text: $options[i])
                    .focused($focusedField, equals: i)
            }
        }
        .onSubmit {
            if let current = focusedField {
                focusedField = current + 1
            }
        }
    }
}

The above code works exactly as expected. You can change the count to any number and tapping return progresses to the next field. Adding any other textfields to this form however breaks this. For instance:

struct TestView: View {
    @State private var title: String = ""
    @State private var description: String = ""
    @State private var options: [String] = Array(repeating: "", count: 7)
    @FocusState private var focusedField: Int?
    
    var body: some View {
        Form {
            TextField("Title", text: $title)
                .focused($focusedField, equals: 0)
            TextField("Description", text: $description)
                .focused($focusedField, equals: 1)
            ForEach($options.indices) { i in
                TextField("Option \(i + 1)", text: $options[i])
                    .focused($focusedField, equals: 2 + i)
            }
        }
        .onSubmit {
            if let current = focusedField {
                print("setting focused field to \(current + 1)")
                focusedField = current + 1
            }
        }
        .onChange(of: focusedField) { newValue in
            print("focused field changed to \(newValue)")
        }
    }
}

In this case, the form stops short of the last field and closes the keyboard. By observing focusedField I can see where when changed to the last field it just is set to nil. The more text fields you add, the earlier this logic breaks. I'm not sure what the issue is.

focused field changed to Optional(0)
setting focused field to 1
focused field changed to Optional(1)
setting focused field to 2
focused field changed to Optional(2)
setting focused field to 3
focused field changed to Optional(3)
setting focused field to 4
focused field changed to Optional(4)
setting focused field to 5
focused field changed to Optional(5)
setting focused field to 6
focused field changed to Optional(6)
setting focused field to 7
focused field changed to Optional(7)
setting focused field to 8
focused field changed to nil
1

There are 1 best solutions below

3
Yrb On

I think you were having some "out of range" error as a guess. Operating on that premise, I came up with the following fix that seems to work fine, and rolls you back to the first TextField upon a return in the last.

struct FocusedForEach: View {
    @State private var title: String = ""
    @State private var description: String = ""
    @State private var options: [String] = Array(repeating: "", count: 7)
    @FocusState private var focusedField: Int?
    
    var body: some View {
        ScrollViewReader { reader in
            Form {
                TextField("Title", text: $title)
                    .id(0)
                    .focused($focusedField, equals: 0)
                TextField("Description", text: $description)
                    .id(1)
                    .focused($focusedField, equals: 1)
                ForEach($options.indices) { i in
                    TextField("Option \(i + 1)", text: $options[i])
                        .id(i + 1)
                        .focused($focusedField, equals: 2 + i)
                }
            }
            .onSubmit {
                if let current = focusedField {
                    if current == options.count + 1 {
                        reader.scrollTo(0, anchor: .bottom)
                        focusedField = 0
                        print("focusedField set to 0")
                    } else {
                        reader.scrollTo(current + 1, anchor: .bottom)
                        focusedField = current + 1
                        print("focusedField set to \(current + 1)")
                    }
                }
            }
            .onChange(of: focusedField) { newValue in
                guard let newValue = newValue else { return }
                print("focused field changed to \(newValue)")
            }
        }
    }
}

You obviously can't set focusedField to 9 as there are not 10 fields. I suspect when you tried to focus on TextField 9, which does not exist, .focused() caused focusedField to revert to nil. It is a hunch, but it seems to be born out in the results.

edit:

Well, it seems that the keyboard is, or is part of, the problem. I edited the code with .scrollTo() to move the fields before they become .focused() so that they are never hidden by the keyboard.