I have a view called TitleList
that allows the user to edit the title of a row using a .swipeAction
(it will replace the NavigationLink
with a TextField
).
While a title is being edited, if a user taps another row's NavigationLink
and a view gets pushed on the root views NavigationStack
, when I return to the root view and try to edit the previous row again, its TextField
doesn't automatically activate despite its FocusState
property being set to true (I have to tap the field for the keyboard to pop up). Here is a video that shows what I'm describing and also here is the full code:
import SwiftUI
struct HomeView: View {
@State private var titles: [String] = ["Title 1", "Title 2", "Title 3"]
var body: some View {
NavigationStack {
VStack {
List {
TitleList(titles: $titles)
}
}
}
}
}
struct TitleList: View {
@Binding var titles: [String]
@State private var renamingTitleIndex: Int?
@State private var renamingTextFieldUserInput: String = ""
@FocusState private var renamingTextFieldIsFocused: Bool
var body: some View {
ForEach(Array(titles.enumerated()), id: \.offset) { index, title in
if renamingTitleIndex != index {
NavigationLink(destination: Text("Test")) {
Text(titles[index])
}
.swipeActions(allowsFullSwipe: false) {
Button {
renamingTextFieldIsFocused = true
renamingTitleIndex = index
renamingTextFieldUserInput = ""
} label: {
Label("Rename", systemImage: "pencil")
}
}
} else {
VStack {
TextField("", text: $renamingTextFieldUserInput)
.focused($renamingTextFieldIsFocused)
.onSubmit {
if !renamingTextFieldUserInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
titles[index] = renamingTextFieldUserInput
}
}
.onDisappear {
renamingTextFieldIsFocused = false
}
.onChange(of: renamingTextFieldIsFocused) { isFocused in
if !isFocused {
renamingTitleIndex = nil
renamingTextFieldUserInput = ""
}
}
}
}
}
}
}
Is there a way to get the FocusState
property to work so I don't have manually tap the TextField
when this happens? I know I could disable all the other rows while a row is editing (I probably will end up doing this regardless), but I'd also like to see if I can find a solution to this problem as I have other NavigationLinks
in other Lists
that could be triggered and would like to avoid having to disable all of them when a TitleList
row is editing.
Here's a more reliable method than using an arbitrary delay. Other than the fact that your textfield doesn't exist in the layout when the button is pressed, the problem is you're overcomplicating the logic.
The logic should be that you have a list with navigation links, the labels of which will be either editable or not, based on a flag indicating which row is editable.
All that the edit button should do is set the flag/state with the value of the row to be edited. Since your rows are not identifiable, in this case it will simply be the index.
Each row view will use this index to determine if it should present an editable textfield or not.
Since your focus state is a Bool
, there is no need for it to be in the List. Instead, each row should have its own FocusState
that will react based on whether the respective row is the one to be edited.
In order for each row to know whether it should be editable or not, typically it would compare the index flag to its own index value. But since each row doesn't have an id or an index, you could pass along the index from the list, or use a custom binding to avoid passing an index. The example below shows the latter.
import SwiftUI
struct FocusStateHomeView: View {
@State private var titles: [String] = ["Title 1", "Title 2", "Title 3"]
var body: some View {
NavigationStack {
VStack {
List {
Section {
FocusStateTitleList(titles: $titles)
} header: {
Text("Available titles")
} footer: {
Text("Swipe to edit a title, press Return when done.")
}
}
}
.navigationTitle("Titles")
}
}
}
struct FocusStateTitleList: View {
@Binding var titles: [String]
@State private var selectedEditingIndex: Int?
var body: some View {
ForEach(Array(titles.enumerated()), id: \.offset) { index, title in
NavigationLink(destination: Text(title)) {
TitleListEditableRow(title: $titles[index], editMode: Binding(
get: { selectedEditingIndex == index },
set: { newValue in
selectedEditingIndex = newValue ? index : nil
}
))
}
.swipeActions(allowsFullSwipe: false) {
Button {
selectedEditingIndex = index
} label: {
Label("Rename", systemImage: "pencil")
}
}
}
}
}
struct TitleListEditableRow: View {
//Parameters
@Binding var title: String
@Binding var editMode: Bool
//Focus states
@FocusState private var focused: Bool
//Body
var body: some View {
TextField("", text: $title)
.focused($focused)
.disabled(!editMode)
.onChange(of: editMode) {
if editMode {
focused = true
}
}
.onSubmit {
editMode = false
}
}
}
#Preview {
FocusStateHomeView()
}
Note how much simpler your ForEach
in TitleList
has become, and the reduced number of states needed.
To clarify, the editMode
custom binding will act as intermediary that will determine whether a row is editable based on the index and the selectedEditIndex
and pass true or false to the view. Similary, it will receive true or false from the view (in this case false), which will set the selectedEditIndex
to nil, when editing is done. This avoids having to pass both the index and a binding to the selectedEditIndex to the view and making that determination inside the view.