5

Automatic keyboard avoidance seems to work fine if it's a regular TextField (i.e. one that doesn't expand on an axis), whether or not it is contained in a ScrollView

Keyboard avoidance also seems to work with the new TextField(_:text:axis) introduced in iOS 16 if it's simply placed in a VStack without being wrapped in a ScrollView. It will even continue to avoid the keyboard correctly as the height expands with more text.

But I can't seem to get keyboard avoidance to work with TextField(_:text:axis) if it is placed inside a ScrollView

I can employ the hacky method of using a ScrollViewReader combined with DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) to wrap the proxy.scrollTo() when the TextField is focused. This sort of works when you first focus the field, but I can't seem to get the ScrollView to continue to adjust its position as the TextField expands.

Here is an example:

struct KeyboardAvoidingView: View {
    @State var text = ""
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                VStack {
                    Color.red
                        .frame(height: 400)
                    Color.blue
                        .frame(height: 400)
                    TextField("Name", text: $text, axis: .vertical)
                        .padding(.vertical)
                        .onTapGesture {
                            DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { 
                                withAnimation(.default) {
                                    proxy.scrollTo(0)
                                }
                            }
                        }
                        .onChange(of: text) { newValue in
                            proxy.scrollTo(0)   // This doesn't seem to do anything
                        }
                    Spacer()
                        .frame(height: 0)
                        .id(0)
                }
            }
        }
    }
}

I guess I'm wondering whether this is expected behavior, or a bug. And regardless if it's one or the other, I'm wondering if I can have an auto-expanding text field inside a scroll view that I can make avoid the keyboard even as the height of the field expands?


UPDATE: It turns out, the issue was with placing the TextField inside a VStack instead of a LazyVStack. I assume ScrollView doesn't know what to do with just a regular VStack in certain situations. If I replace the VStack with a LazyVStack in my example, everything works as expected!

Adam Singer
  • 2,377
  • 3
  • 18
  • 18

2 Answers2

4

I answered the question with the update posted above. The issue was with using VStack instead of LazyVStack

Adam Singer
  • 2,377
  • 3
  • 18
  • 18
0

This is a long time known bug in the TextField component, but you may achieve the desired behavior by using an anchor: .bottom in the proxy.scrollTo call of your onChange.

it'll look like this:

// ...
TextField("Name", text: $text, axis: .vertical)
    .padding(.vertical)
    .onTapGesture {
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { 
            withAnimation(.default) {
                proxy.scrollTo(0)
            }
        }
    }
    .onChange(of: text) { newValue in
        // This will always scroll to the bottom of the text editor, 
        // just make sure to pass the right value in the first parameter
        // that will identify your TextEditor
        proxy.scrollTo(MyTextEditorId, anchor: .bottom)   
    }
// ...

You may need some additional work to handle the editing of upper parts of the text editor when it's taller than your screen

Leu
  • 1,354
  • 1
  • 10
  • 19
  • 1
    Surprisingly, this does not work. If I start typing without further scrolling, the ScrollView does not automatically scroll to the bottom of the vertically-expanding TextField. If I manually scroll the ScrollView a little bit, then the next time I type a key, it will correctly scroll to the bottom. Not sure if this is a bug or if I'm still not doing something correctly. I do have the proxy scrolling to the ID of the TextField itself. And again, I do not have this problem at all if the TextField does not have an `axis` parameter, nor if the TextField with `axis` is not inside a ScrollView. – Adam Singer Feb 14 '23 at 22:38
  • Sorry to hear that. This issue has been hunting one of my projects for a while now. We had come back and forth in using the SwiftUI's TextField or having a UIViewRepresentable wrapping a UIKit's UITextField. This solution was the midterm :T – Leu Feb 16 '23 at 01:13