SwiftUI 中的条件 onTapGesture
Conditional onTapGesture in SwiftUI
我有一个导航 link,当它的标签 (MyView
) 被点击时,我需要不同的行为,具体取决于编辑模式(或任何其他条件):
- 如果我们不处于编辑模式,我想触发导航link并显示
DetailView
所选模型。
- 如果我们处于编辑模式,我不想触发导航 link 而是在模式 sheet 中显示
EditingView
。
这是我想出的一种实现方法:
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)
}
这种方法的问题在于 if- 和 else- 分支中的视图类型不同(由于 onTapGesture
修饰符)并且 SwiftUI 无法将它们识别为相同的视图.因此,动画不能被插值并且不能正常工作。此外,每次切换 editMode
时,MyView
总是会丢失其状态。
(Chris Eidhof 对此做了很好的解释:https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/)
所以我继续将 if 语句移动到 onTapGesture
修饰符中,如下所示,这样我就没有两个不同的 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)
}
问题是现在要求 #1 不再起作用:onTapGesture
完全吞没了点击手势,因此导航 link 永远不会被触发以显示 DetailView
.有道理。
现在我的问题是:
如何在没有这些缺点的情况下获得所需的行为?
总之,你想改变:
if editMode == .active {
MyView()
.onTapGesture {
editingModel = model
}
} else {
MyView()
}
进入:
MyView()
.allowsHitTesting(editMode == .active)
.onTapGesture {
editingModel = model
}
这解决了这个问题,因为现在 onTapGesture
仅在它可以实际侦听触摸时才会触发。它只能在 editMode == .active
时触发,否则命中测试将被禁用。
完整示例:
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
}
将内部标签位更改为:
MyView()
.allowsHitTesting(editMode == .active)
.onTapGesture {
editingModel = EditingModel(tag: "model")
}
根据 George 的建议,我最终得到了以下自定义修饰符:
extension View {
func onTapGestureIf(_ condition: Bool, closure: @escaping () -> ()) -> some View {
self.allowsHitTesting(condition)
.onTapGesture {
closure()
}
}
}
非常有效,这样一来,我就不必指定相同的条件 editMode == .active
两次(单一事实来源)。强烈推荐。
MyView()
.onTapGestureIf(editMode == .active) {
editingCounter = counter
}
我有一个导航 link,当它的标签 (MyView
) 被点击时,我需要不同的行为,具体取决于编辑模式(或任何其他条件):
- 如果我们不处于编辑模式,我想触发导航link并显示
DetailView
所选模型。 - 如果我们处于编辑模式,我不想触发导航 link 而是在模式 sheet 中显示
EditingView
。
这是我想出的一种实现方法:
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)
}
这种方法的问题在于 if- 和 else- 分支中的视图类型不同(由于 onTapGesture
修饰符)并且 SwiftUI 无法将它们识别为相同的视图.因此,动画不能被插值并且不能正常工作。此外,每次切换 editMode
时,MyView
总是会丢失其状态。
(Chris Eidhof 对此做了很好的解释:https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/)
所以我继续将 if 语句移动到 onTapGesture
修饰符中,如下所示,这样我就没有两个不同的 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)
}
问题是现在要求 #1 不再起作用:onTapGesture
完全吞没了点击手势,因此导航 link 永远不会被触发以显示 DetailView
.有道理。
现在我的问题是:
如何在没有这些缺点的情况下获得所需的行为?
总之,你想改变:
if editMode == .active {
MyView()
.onTapGesture {
editingModel = model
}
} else {
MyView()
}
进入:
MyView()
.allowsHitTesting(editMode == .active)
.onTapGesture {
editingModel = model
}
这解决了这个问题,因为现在 onTapGesture
仅在它可以实际侦听触摸时才会触发。它只能在 editMode == .active
时触发,否则命中测试将被禁用。
完整示例:
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
}
将内部标签位更改为:
MyView()
.allowsHitTesting(editMode == .active)
.onTapGesture {
editingModel = EditingModel(tag: "model")
}
根据 George 的建议,我最终得到了以下自定义修饰符:
extension View {
func onTapGestureIf(_ condition: Bool, closure: @escaping () -> ()) -> some View {
self.allowsHitTesting(condition)
.onTapGesture {
closure()
}
}
}
非常有效,这样一来,我就不必指定相同的条件 editMode == .active
两次(单一事实来源)。强烈推荐。
MyView()
.onTapGestureIf(editMode == .active) {
editingCounter = counter
}