While I've seen examples of using $FocusState with Lists containing "raw" TextFields, in my use case, the List rows are more complex than that and contain multiiple elements, including TextFields. I obviously don't understand something fundamental here, because I am completely unable to get TextField-to-TextField tabbing to work. Can someone set me straight?
Sample code demonstrating the issues:
//
// ContentView.swift
// ListElementFocus
//
// Created by Richard Aurbach on 10/12/24.
//
import SwiftUI
/// NOTE: in my actual app, the data model is actually a set of SwiftData
/// PresistentModel objects. Here, I'm simulating them with an Observable.
@Observable
final class TestModel: Identifiable {
public var id: UUID
public var checked: Bool = false
public var title: String = "Test"
public var subtitle: String = "Subtitle"
init(checked: Bool = false, title: String, subtitle: String) {
self.id = UUID()
self.checked = checked
self.title = title
self.subtitle = subtitle
}
}
struct ContentView: View {
/// Instead of a @Query...
@State var records: [TestModel] = [
TestModel(title: "First title", subtitle: "blah, blah, blah"),
TestModel(title: "Second title", subtitle: "more nonsense"),
TestModel(title: "Third title", subtitle: "even more nonsense"),
]
@FocusState var focus: UUID?
var body: some View {
Form {
Section {
HStack(alignment: .top) {
Text("Goal:").font(.headline)
Text(
"If a user taps in the TextField in any row, they should be able to tab from row to row using any keyboard which supports a tab key."
)
}
HStack(alignment: .top) {
Text("#1:").font(.headline)
Text(
"While I will admit that this code is probaby total garbage, I haven't been able to find any way to make tabbing from row to row to work at all."
)
}
HStack(alignment: .top) {
Text("#2:").font(.headline)
Text(
"Tapping the checkbox button causes the row to flash with the current accent color, and I can't find any way to turn that off."
)
}
} header: {
Text("Problems").font(.title3).bold()
}.headerProminence(.increased)
Section {
List(records) { record in
ListRow(record: record, focus: focus)
.onKeyPress(.tab) {
focus = next(record)
return .handled
}
}
} header: {
Text("Example: Selector of Editable Items").font(.title3).bold()
}.headerProminence(.increased)
}
.padding()
}
private func next(_ record: TestModel) -> UUID {
guard !records.isEmpty else { return UUID() }
if record.id == records.last!.id { return records.first!.id }
if let index = records.firstIndex(where: { $0.id == record.id }) {
return records[index + 1].id
}
return UUID()
}
}
struct ListRow: View {
@Bindable var record: TestModel
var focus: UUID?
@FocusState var focusState: Bool
init(record: TestModel, focus: UUID?) {
self.record = record
self.focus = focus
self.focusState = focus == record.id
}
var body: some View {
HStack(alignment: .top) {
Button {
record.checked.toggle()
} label: {
record.checked
? Image(systemName: "checkmark.square.fill")
: Image(systemName: "square")
}.font(.title2).focusable(false)
VStack(alignment: .leading) {
TextField("title", text: $record.title).font(.headline)
.focused($focusState)
Text("subtitle").italic()
}
}
}
}
#Preview {
ContentView()
}