Plato
Plato

Reputation: 641

SwiftUI @FocusState not working in ScrollView

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?

Upvotes: 2

Views: 1298

Answers (1)

Jono Forbes
Jono Forbes

Reputation: 121

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")
            }
        }
    }
}

Upvotes: 0

Related Questions