I have a navigation link and I need a different behavior when its label (MyView
) is tapped depending on the edit mode (or any other condition):
DetailView
with the selected model.EditingView
in a modal sheet instead.Here's a way to implement this that I came up with:
NavigationLink(tag: model, selection: $displayedItem) {
DetailView(model: model)
} label: {
if editMode == .active {
MyView()
.onTapGesture {
editingModel = model
}
} else {
MyView()
}
}
.sheet(item: $editingModel) { model in
EditingView(model: model)
}
The problem with this approach is that the views in the if- and the else-branch have not the same type (due to the onTapGesture
modifier) and SwiftUI doesn't recognize them as the same view. Thus, animations cannot be interpolated and don't work properly. Also, MyView
always loses its state each time editMode
is toggled.
(Here's a great explanation from Chris Eidhof on why that happens: https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/)
So I went ahead and moved the if-statement inside the onTapGesture
modifier as follows so that I don't have two different MyView
s:
NavigationLink(tag: model, selection: $displayedItem) {
DetailView(model: model)
} label: {
MyView()
.onTapGesture {
if editMode == .active { // moved
editingModel = model
} // moved
}
}
}
.sheet(item: $editingModel) { model in
EditingView(model: model)
}
Problem with this is that now requirement #1 doesn't work anymore: The onTapGesture
completely swallow the tap gesture and thus the navigation link is never trigged to show the DetailView
. Makes sense.
Now my question is:
In short, you want to change:
if editMode == .active {
MyView()
.onTapGesture {
editingModel = model
}
} else {
MyView()
}
Into:
MyView()
.allowsHitTesting(editMode == .active)
.onTapGesture {
editingModel = model
}
This fixes the issue because now the onTapGesture
is only triggered when it can actually listen to touches. It can only trigger when editMode == .active
, because otherwise the hit testing is disabled.
Full example:
struct ContentView: View {
@State private var displayedItem: String?
@State private var editingModel: EditingModel?
@State private var editMode: EditMode = .inactive
var body: some View {
NavigationView {
List {
Button("Edit mode: \(editMode == .active ? "active" : "inactive")") {
if editMode == .active {
editMode = .inactive
} else {
editMode = .active
}
}
NavigationLink(tag: "model", selection: $displayedItem) {
Text("DetailView")
} label: {
if editMode == .active {
MyView()
.onTapGesture {
editingModel = EditingModel(tag: "model")
}
} else {
MyView()
}
}
.sheet(item: $editingModel) { model in
Text("EditingView: \(model.tag)")
}
}
}
}
}
struct MyView: View {
var body: some View {
Text("MyView")
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
}
struct EditingModel: Identifiable {
var id: String { tag }
let tag: String
}
Changing the inner label bit to:
MyView()
.allowsHitTesting(editMode == .active)
.onTapGesture {
editingModel = EditingModel(tag: "model")
}