skip to Main Content

This code works great, just as it should. isFocused reflects the focus state of the text field, and pressing the Button drops the keyboard.

struct ContentView: View {
    @State private var textInput = ""
    @FocusState private var isFocused: Bool
    
    var body: some View {
        VStack {
            TextField("Enter text", text: $textInput)
                .focused($isFocused)
            
            Button("Submit") {
                isFocused = false
            }
        }
    }
}

However, putting the TextField instead a ScrollView results in @FocusState NOT working. When the button is tapped, "Dismiss" is printed but the keyboard does not resign.

struct ContentView: View {
    @State private var textInput = ""
    @FocusState private var isFocused: Bool

    var body: some View {
        ScrollView(.vertical) {
            TextField("Enter text", text: $textInput)
                .focused($isFocused)
            Button("Dismiss") {
                isFocused = false
                print("Dismiss")
            }
        }
    }
}

Why is this the case? And how could this be fixed?

2

Answers


  1. I’m currently experiencing the exact same problem on Xcode 15 beta 4, the same code works just fine in Xcode 14. Filed a bug report and hopefully it gets resolved in future versions.

    Login or Signup to reply.
  2. Experiencing the same still with Xcode 15 beta 6.

    A temporary workaround (though not ideal) is to replace your ScrollView with a VStack inside a List with listStyle set to plain, row separators set to hidden and row insets as (0, 0, 0, 0).

    i.e.

    struct ContentView: View {
        @State private var textInput = ""
        @FocusState private var isFocused: Bool
        
        var body: some View {
            List {
                VStack {
                    TextField("Enter text", text: $textInput)
                        .focused($isFocused)
                    Button("Dismiss") {
                        isFocused = false
                        print("Dismiss")
                    }
                }
                .listRowSeparator(.hidden)
                .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
                .padding(.horizontal)
            }
            .listStyle(.plain)
        }
    }
    

    This should produce identical results to a ScrollView. To avoid repeating the workaround pattern, here’s a quick backporting wrapper.

    struct BackportScrollView<Content: View>: View {
        var content: () -> Content
        init(@ViewBuilder content: @escaping () -> Content) { self.content = content }
        
        var body: some View {
            if #available(iOS 17.0, *) {
                List {
                    VStack { content() }
                        .listRowSeparator(.hidden)
                        .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
                        .padding(.horizontal)
                }
                .listStyle(.plain)
            } else {
                ScrollView(.vertical) { content() }
            }
        }
    }
    

    You can use this like so:

    struct ContentView: View {
        @State private var textInput = ""
        @FocusState private var isFocused: Bool
        
        var body: some View {
            BackportScrollView {
                TextField("Enter text", text: $textInput)
                    .focused($isFocused)
                Button("Dismiss") {
                    isFocused = false
                    print("Dismiss")
                }
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search