'Is there a SwiftUI "pure" way of auto scroll a List to show a TextField consistently? (keyboard is hiding the field)

The Context:

I have a List row that, in some cases, shows a TextField (depending on a switch condition). I managed to solve the focus problem with an excellent article from Peter Friese Link to article that suggests a very clever use of enums Associated fields.

The Issue:

BUT I'm facing an inconsistent scrolling behavior 😥 that can be seen in this screen capture:

iOS sim capture os scrolling issue

The relevant code:

            List($roundQuestions) { $question in
                HStack {
                    Group {
                        Image(systemName: "\(question.index ).circle")
                            .foregroundColor(questionColor(question: question))
                            .scaleEffect(0.6)
                        Text("\(question.factorA)")
                        Text("x")
                        question.status == .unrevealed ? Text("?") : Text("\(question.factorB)")
                        Text("=")
                        switch question.status {
                        case .unrevealed:
                            if question.index == currentQuestionArrayIndex + 1 {
                                Spacer()
                                Button("Go!") {
                                    question.status = .active
                                }
                                .buttonStyle(.borderedProminent)
                                .font(.none)
                                .lineLimit(1)
                            }
                        case .active:
                            ZStack (alignment: .trailing) {
                                TextField("??", text: $questionGuess )
                                    .textFieldStyle(.roundedBorder)
                                    .keyboardType(.numberPad)
                                    .focused($focusedQuestion, equals: .row(id: question.id))
                                    .onAppear {
                                        focusedQuestion = .row(id: question.id)
                                        print("onAppear question.id = \(question.index)")
                                    }
                                    .onDisappear {
                                        print("onDisappear question.id = \(question.index)")
                                    }
                            }
                            
                            Button("?") {
                                processUserGuess()
                            }
                            .buttonStyle(.borderedProminent)
                            .font(.none)
                        case .error:
                            Text("\(question.productGuess)").foregroundColor(.red)
                        case .right:
                            Text("\(question.productGuess)").foregroundColor(.green)
                        default:
                            Text("__")
                        }
                    }
                    .font(.custom("SF Compact", size: 40, relativeTo: .largeTitle))
                    .padding(.vertical)
                }
            }

The entire repo (for those who goes deep):

https://github.com/gilsoncav/tabulenzo

Observation #1:

I'm aware of solution paths like Move TextField up when the keyboard has appeared in SwiftUI but they seem "hacky" and the code doesn't "read well" in my opinion

Observation #2:

I'm a Swift, native iOS and SwiftUI newbie. 🤗



Solution 1:[1]

There is another option, but it uses ScrollView instead of List, and then facilitates ScrollViewReader to scroll to the active question. It works, but you would have to do some custom formatting to get the same look as before (I already added some Spacers to get the overall look):

            // Scrollview & reader instead of List
            ScrollViewReader { scrollProxy in
                ScrollView {
                    ForEach($roundQuestions) { $question in
                        HStack {
                            Group {
                                Image(systemName: "\(question.index ).circle")
                                    .foregroundColor(questionColor(question: question))
                                    .scaleEffect(0.6)
                                Text("\(question.factorA)")
                                Text("x")
                                question.status == .unrevealed ? Text("?") : Text("\(question.factorB)")
                                Text("=")
                                switch question.status {
                                case .unrevealed:
                                    if question.index == currentQuestionArrayIndex + 1 {
                                        Spacer()
                                        Button("Go!") {
                                            question.status = .active
                                        }
                                        .buttonStyle(.borderedProminent)
                                        .font(.none)
                                        .lineLimit(1)
                                    } else {
                                        Spacer()
                                    }
                                case .active:
                                    HStack  {
                                        TextField("??", text: $questionGuess )
                                            .textFieldStyle(.roundedBorder)
                                            .keyboardType(.numberPad)
                                            .focused($focusedQuestion, equals: .row(id: question.id))
                                            .onAppear {
                                                focusedQuestion = .row(id: question.id)
                                                // NEW: Scroll to active question
                                                withAnimation {
                                                    scrollProxy.scrollTo(question.id, anchor: .bottom)
                                                }
                                            }
                                    }
                                    
                                    Button("?") {
                                        processUserGuess()
                                    }
                                    .buttonStyle(.borderedProminent)
                                    .font(.none)
                                case .error:
                                    Text("\(question.productGuess)").foregroundColor(.red)
                                    Spacer()
                                case .right:
                                    Text("\(question.productGuess)").foregroundColor(.green)
                                    Spacer()
                                default:
                                    Text("__")
                                    Spacer()
                                }
                            }
                            .font(.custom("SF Compact", size: 40, relativeTo: .largeTitle))
                            .padding(.vertical)
                        }
                        // new: id for scrollto
                        .id(question.id)
                    }
                }
            }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ChrisR