I have a SwiftUI List
(on macOS) where I want to display a few TextFields
in each row, and observe how the focus changes as an individual text field is selected or unselected. This seems to work fine if I just have the List
display a pure TextField
in each row, but if I embed the TextField
inside a Button
view, the focus change notifications stop working. I want the 'button' functionality so that the user can tap anywhere on the row and have the text field activated, instead of tapping exactly on the text field boundary.
Here is some code to demonstrate the problem. In the List
if you have RowView
uncommented out (1), then the focus change notifications work. If you comment that out and uncomment the RowViewWithButton
(2), then the button functionality works, but I can't get focus change notifications anymore.
import SwiftUI
// test on macOS target
struct ContentView: View {
@State private var textFields = Array(repeating: "", count: 4)
@FocusState private var focusedField: Int?
var body: some View {
List(0..<4, id: \.self) { index in
// 1. works with focus notification changes
RowView(index: index, text: $textFields[index], focusedField: $focusedField)
// 2. button works, but no focus notifications on text field
//RowViewWithButton(index: index, text: $textFields[index], focusedField: $focusedField)
}
}
}
struct RowView: View {
let index: Int
@Binding var text: String
@FocusState.Binding var focusedField: Int?
var body: some View {
HStack {
Text("Row \(index + 1):")
TextField("", text: $text)
.multilineTextAlignment(.leading)
.focused($focusedField, equals: index)
}
.onChange(of: focusedField) { newFocus in
if newFocus == index {
print("TextField \(index) is in focus")
} else {
print("TextField \(index) lost focus")
}
}
.padding(.vertical, 4)
}
}
struct RowViewWithButton: View {
let index: Int
@Binding var text: String
@FocusState.Binding var focusedField: Int?
var body: some View {
Button (action: {
print("RowView - button selected at index \(index)")
focusedField = index
}) {
HStack {
Text("Row \(index + 1):")
TextField("", text: $text)
.multilineTextAlignment(.leading)
.focused($focusedField, equals: index)
}
.onChange(of: focusedField) { newFocus in
if newFocus == index {
print("TextField \(index) is in focus")
} else {
print("TextField \(index) lost focus")
}
}
.foregroundColor(.primary)
}
.buttonStyle(BorderlessButtonStyle())
.padding(.vertical, 4)
}
}
#Preview {
ContentView()
.frame(width: 320, height: 250)
}
Interestingly, when I test this on iOS, it works fine in both cases. But I need the solution for macOS.
UPDATE:
Using onTapGesture
instead of a Button
does improve things, but introduces a new bug. If I tap on the row initially, it initially works fine, and tapping anywhere will activate the text field. But after typing in some text and pressing the return key (or losing focus in any way), if I try to tap on the part of the row where the TextField
is, it only works if tapped on the existing text ... not on the empty space in front of it.
I added .background(.red)
to the HStack, and .background(.blue)
and added a screenshot to demonstrate the issue.
From your comment,
I want the 'button' functionality so that the user can tap anywhere on the row and have the text field activated, instead of tapping exactly on the text field boundary
.
You can achieve this without using a Button
.
Try this approach using a .overlay(...)
on the HStack
to capture the taps on a list row, and still be able to check
the change of focus at the same time.
Example code, tested on MacOS 15.4
struct ContentView: View {
@State private var textFields = Array(repeating: "", count: 4)
var body: some View {
List(0..<4, id: \.self) { index in
RowView(index: index, text: $textFields[index])
}
.listStyle(.plain)
}
}
struct RowView: View {
let index: Int
@Binding var text: String
@FocusState var focusedField: Int?
var body: some View {
HStack {
Text("Row \(index)").padding()
TextField("", text: $text).border(.red)
.multilineTextAlignment(.leading)
.focused($focusedField, equals: index)
}
.border(.green)
.contentShape(Rectangle())
.overlay(
Color.white.opacity(0.001).ignoresSafeArea()
.onTapGesture {
print("---> Tapped on \(index)")
focusedField = index
}
)
.onChange(of: focusedField) {
if focusedField == index {
print("TextField \(index) is in focus")
} else {
print("TextField \(index) lost focus")
}
}
.padding(.vertical, 4)
}
}