SwiftUI 中的条件 onTapGesture

Conditional onTapGesture in SwiftUI

我有一个导航 link,当它的标签 (MyView) 被点击时,我需要不同的行为,具体取决于编辑模式(或任何其他条件):

  1. 如果我们处于编辑模式,我想触发导航link并显示DetailView所选模型。
  2. 如果我们处于编辑模式,我不想触发导航 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 修饰符中,如下所示,这样我就没有两个不同的 MyViews:

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
    }