3

What I'm trying to accomplish

A vertical list of TextField views that user enters values in one at a time. After entering a value in a text field and pressing keyboard submit button the focus moves to the next text field.

The problem I'm facing

The keyboard obstructs the active text field. I was hoping that if I used ScrollView it would automagically scroll to the next focused text field, but that's not the case.

Then I decided to try using ScrollViewReader's ScrollViewProxy object to scroll to the focused text field, which works! But not quite. It scrolls to the selected text field, but not enough to go over the keyboard. It seems to always be just a little bit underneath the top of the keyboard (regardless of whether there's keyboard toolbar or autocorrection bar or nothing).

Here's a screenshot of roughly where the text field ends up after I call proxy.scrollTo(focusedInput):

Here's where I would want it to end up:

Minimal code sample

struct ContentView: View {
    @State var inputsValues: [String]
    @FocusState var focusedInput: Int?
    
    init() {
        inputsValues = (0..<30).map { _ in "" }
    }
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                VStack {
                    ForEach((0..<inputsValues.count), id: \.self) { i in
                        TextField("Value", text: $inputsValues[i])
                            .focused($focusedInput, equals: i)
                            .submitLabel(.next)
                            .id(i)
                            .onSubmit {
                                if (i + 1) < inputsValues.count {
                                    focusedInput = i + 1
                                    proxy.scrollTo(focusedInput)
                                } else {
                                    focusedInput = nil
                                }
                            }
                    }
                }
            }
        }.toolbar {
            ToolbarItem(placement: .keyboard) {
                Text("This is toolbar")
            }
        }
    }
}
Alex
  • 1,574
  • 17
  • 36

1 Answers1

4

Chaning two+ states in one closure (when they are related somehow or affects one area) are handled not very good (or not reliable). The fix is to separate them in different handlers.

Tested with Xcode 13.3 / iOS 15.4

demo

Here is main part:

VStack {
   // ... other code

            .onSubmit {
                // update state here !!
                if (i + 1) < inputsValues.count {
                    focusedInput = i + 1
                } else {
                    focusedInput = nil
                }
            }
    }
}
.onChange(of: focusedInput) {
    // react on state change here !!
    proxy.scrollTo($0)
}

Complete test module is here

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    Legendary! Thanks Asperi, I don't know how you do it every time. I wanted to support you on Ko-fi, but it says transactions are disabled to comply with regulations. – Alex Apr 26 '22 at 18:48
  • Hi Asperi, I am having a similar issue to the one mentioned above. I was wondering if you could kindly take a look! its driving me mad! Thank you in advance.. https://stackoverflow.com/questions/74364476/swiftui-multiline-textfield-with-vertical-axis-going-below-keyboard-with-scroll – Charles Nov 08 '22 at 17:02
  • 1
    Hi @Asperi if you add a simple border around the TextField, it will only show half of the field, is there a way to ask the ScrollViewReader to scroll a little more up? Thank you. – Wael Dec 01 '22 at 21:38
  • This solution doesn't work if we need to have a button at the bottom of a form which has a ScrollView and the button should always be visible using .ignoresSafeArea(.keyboard) I've gone thru every single possible solution in another thread and none of them work correctly. https://stackoverflow.com/questions/56491881/move-textfield-up-when-the-keyboard-has-appeared-in-swiftui especially with a TextEditor control. – Wael Dec 06 '22 at 04:11