I have this following code:
NavigationView {
List {
ForEach(someList) { someElement in
NavigationLink(destination: someView()) {
someRowDisplayView()
} //: NavigationLink
} //: ForEach
} //: List
} //: NavigationView
Basically, it displays a dynamic list of class objects previously filled by user input. I have an "addToList()" function that allows users to add some elements (let's say name, email, whatever) and once the user confirms it adds the element to this list. Then I have a view that displays the list through a ForEach and then makes a NavigationLink of each element as indicated in the code example above.
Once clicked on an element of this list, the user should be properly redirected to a destination view as the NavigationLink implies. All this is working fine.
Here's where I face an issue: I want the user to be able to edit the content of a row of this list without having to delete the row and re-add another one.
I decided to use the EditMode() feature of SwiftUI. So this is what I came up with:
@State private var editMode = EditMode.inactive
[...]
NavigationView {
List {
ForEach(someList) { someElement in
NavigationLink(destination: someView()) {
someRowDisplayView()
} //: NavigationLink
} //: ForEach
} //: List
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
.environment(\.editMode, $editMode)
} //: NavigationView
The EditMode is properly triggered when I click on the Edit button. But I noticed that the list row is not clickable in edit mode, which is fine because I do not want it to follow the NavigationLink while in edit mode.
Though what I want is that the user is either redirected to a view that allows editing of the tapped row, or better, that an edition sheet is presented to the user.
Since I couldn't tap the row in edition mode, I have tried several tricks but none of them concluded as I wanted. Here are my tries.
I tried to add a .simultaneousGesture modified to my NavigationLink and toggle a @State variable in order to display the edition sheet.
@State private var isShowingEdit: Bool = false
[...]
.simultaneousGesture(TapGesture().onEnded{
isShowingEdit = true
})
.sheet(isPresented: $isShowingEdit) {
EditionView()
}
This simply does not work. It seems that this .simultaneousGesture somehow breaks the NavigationLink, the tap succeeds like once out of five times.
It doesn't work even by adding a condition on the edit mode, like:
if (editMode == .active) {
isShowingEdit = true
}
In non-edition mode the NavigationLink is still bugged. But I have noticed that once in edition mode, it kind of does what I wanted.
After the previous failure my first thought was that I needed the tap gesture to be triggered only in edit mode, so that in non-edit mode the view doesn't even know that it has a tap gesture.
I decided to add an extension to the View structure in order to define conditions to modifiers (I found this code sample somewhere on Stackoverflow):
extension View {
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
Then in my main code, instead of the previously tried .simultaneousGesture modifier, I added this to NavigationLink:
.if(editMode == .active) { view in
view.onTapGesture {
isShowingEdit = true
}
}
And it worked, I was able to display this edition view once a user taps on a row while in edition mode and the normal non-edition mode still worked as it properly triggered the NavigationLink.
But something bothered me, it didn't feel like a natural or native behavior. Once tapped, the row didn't show any feedback like it shows in non-edition mode when following the NavigationLink: it doesn't highlight even for a very short time.
The tap gesture modifier simply executes the code I have asked without animation, feedback or anything.
I would like the user to know and see which row was tapped and I would like it to highlight just as it does when when tapping on the NavigationLink: simply make it look like the row was actually "tapped". I hope it makes sense.
I would also like the code to be triggered when the user taps on the whole row and not only the parts where the text is visible. Currently, tapping on an empty field of the row does nothing. It has to have text on it.
An even better solution would be something that prevents me from applying such conditional modifiers and extensions to the View structure as I surely prefer a more natural and better method if this is possible.
I'm new to Swift so maybe there is a lot easier or better solution and I'm willing to follow the best practices.
How could I manage to accomplish what I want in this situation?
Thank you for your help.
I am currently using the .onDelete and .onMove implementations on the List alongside with the SwiftUI edition mode and I want to keep using them.
I am developing the app for minimum iOS 14, using Swift language and SwiftUI framework.
If you need more code samples or better explanations please feel free to ask and I will edit my question.
Asked by Yrb.
import SwiftUI
import PlaygroundSupport
extension View {
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
struct SomeList: Identifiable {
let id: UUID = UUID()
var name: String
}
let someList: [SomeList] = [
SomeList(name: "row1"),
SomeList(name: "row2"),
SomeList(name: "row3")
]
struct ContentView: View {
@State private var editMode = EditMode.inactive
@State private var isShowingEditionSheet: Bool = false
var body: some View {
NavigationView {
List {
ForEach(someList) { someElement in
NavigationLink(destination: EmptyView()) {
Text(someElement.name)
} //: NavigationLink
.if(editMode == .active) { view in
view.onTapGesture {
isShowingEditionSheet = true
}
}
} //: ForEach
.onDelete { (indexSet) in }
.onMove { (source, destination) in }
} //: List
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
.environment(\.editMode, $editMode)
.sheet(isPresented: $isShowingEditionSheet) {
Text("Edition sheet")
}
} //: NavigationView
}
}
PlaygroundPage.current.setLiveView(ContentView())
As we discussed in the comments, your code does everything in your list except highlight the row when the disclosure chevron is tapped. To change the background color of a row, you use .listRowBackground()
. To make just one row highlight, you need to make the row identifiable off of the id
in your array in the ForEach
. You then have an optional @State
variable to hold the value of the row id, and set the row id in your .onTapGesture
. Lastly, you use a DispatchQueue.main.asyncAfter()
to reset the variable to nil after a certain time. This gives you a momentary highlight.
However, once you go this route, you have to manage the background highlighting for your NavigationLink
as well. This brings its own complexity. You need to use the NavigationLink(destination:,isActive:,label:)
initializer, create a binding with a setter and getter, and in the getter, run your highlight code as well.
struct ContentView: View {
@State private var editMode = EditMode.inactive
@State private var isShowingEditionSheet: Bool = false
@State private var isTapped = false
@State private var highlight: UUID?
var body: some View {
NavigationView {
List {
ForEach(someList) { someElement in
// You use the NavigationLink(destination:,isActive:,label:) initializer
// Then create your own binding for it
NavigationLink(destination: EmptyView(), isActive: Binding<Bool>(
get: { isTapped },
// in the set, you can run other code
set: {
isTapped = $0
// Set highlight to the row you just tapped
highlight = someElement.id
// Reset the row id to nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
highlight = nil
}
}
)) {
Text(someElement.name)
} //: NavigationLink
// identify your row
.id(someElement.id)
.if(editMode == .active) { view in
view.onTapGesture {
// Set highlight to the row you just tapped
highlight = someElement.id
// Reset the row id to nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
highlight = nil
}
isShowingEditionSheet = true
}
}
.listRowBackground(highlight == someElement.id ? Color(UIColor.systemGray5) : Color(UIColor.systemBackground))
} //: ForEach
.onDelete { (indexSet) in }
.onMove { (source, destination) in }
} //: List
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
.environment(\.editMode, $editMode)
.sheet(isPresented: $isShowingEditionSheet) {
Text("Edition sheet")
}
} //: NavigationView
}
}