我遇到了同样的问题,当我搜索解决方案时,这个问题基本上是排名第一的。因此,既然找到了一个相当优雅的解决方案,这似乎是一个好地方来分享。
问题似乎在于NavigationLink
的手势安装方式使其忽略其子视图手势:它实际上使用了.gesture(_:including:)
和GestureMask
的.gesture
,这意味着它将优先于任何类似优先级的内部手势。
最终,您需要的是一个带有.highPriorityGesture()
的按钮来触发其操作,同时保持按钮的常规API:传递单个操作方法以在触发时运行,而不是定义一个简单的图像并摆弄手势识别器来更改状态等。要使用标准的Button
行为和API,您需要深入了解一些按钮的行为,并直接定义其中一些行为。幸运的是,SwiftUI提供了适当的钩子来实现这一点。
有两种方法可以自定义按钮的外观:您可以实现ButtonStyle
或PrimitiveButtonStyle
,然后将其传递到.buttonStyle()
修饰符中。这两种协议类型都允许您定义一种方法,该方法应用于按钮的Label
视图,无论它被定义为什么。在常规的ButtonStyle
中,您会收到标签视图和一个isPressed
布尔值,使您能够根据按钮的按下状态更改外观。 PrimitiveButtonStyle
提供了更多的控制,但需要跟踪触摸状态:您会收到标签视图和一个trigger()
回调,您可以调用该回调来触发按钮的操作。
后者是我们想要的:我们将拥有按钮的手势,并能够跟踪触摸状态并确定何时触发它。然而,对于我们的用例,我们负责将该手势附加到视图上-这意味着我们可以使用.highPriorityGesture()
修饰符来执行此操作,并使按钮比链接本身具有更高的优先级。
对于一个简单的按钮来说,实现起来相当简单。这是一种简单的按钮样式,它使用内部视图来管理按下/按住状态,使按钮在触摸时半透明,并使用高优先级手势来实现按下/触发状态的更改:
struct HighPriorityButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}
private struct MyButton: View {
@State var pressed = false
let configuration: PrimitiveButtonStyle.Configuration
var body: some View {
let gesture = DragGesture(minimumDistance: 0)
.onChanged { _ in self.pressed = true }
.onEnded { value in
self.pressed = false
if value.translation.width < 10 && value.translation.height < 10 {
self.configuration.trigger()
}
}
return configuration.label
.opacity(self.pressed ? 0.5 : 1.0)
.highPriorityGesture(gesture)
}
}
}
工作发生在内部的
MyButton
视图类型中。它维护一个
pressed
状态,定义了一个拖动手势(用于跟踪下/上/结束事件),该手势切换该状态并在手势结束时从样式配置调用
trigger()
方法(前提是它仍然看起来像一个“轻触”)。然后,它返回按钮提供的标签(即原始
Button
的
label: {}
参数的内容),附加了两个新的修饰符:一个
.opacity()
,其值为
1
或
0.5
,具体取决于按压状态,以及定义的手势作为
.highPriorityGesture()
。
如果您不想为触摸按下状态提供不同的外观,则可以使此过程变得更简单。如果没有需要保存该状态的
@State
属性,您可以在
makeBody()
实现中完成所有操作:
struct StaticHighPriorityButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View {
let gesture = TapGesture()
.onEnded { _ in configuration.trigger() }
return configuration.label
.opacity(pressed ? 0.5 : 1.0)
.highPriorityGesture(gesture)
}
}
最后,这是我用来测试上面第一个实现的预览。在实时预览中运行,你会看到按钮文本在触摸按下时变暗。
struct HighPriorityButtonStyle_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
List {
NavigationLink(destination: Text("Hello")) {
Button(action: { print("hello") }) {
Text("Button!")
.foregroundColor(.accentColor)
}
.buttonStyle(HighPriorityButtonStyle())
}
}
}
}
}
请注意,如果您想在用户开始拖动等情况下取消手势,则需要进行更多的工作。这是一个完全不同的问题。根据手势值的“translation”属性相对简单地更改“pressed”状态,只有在按下时结束才触发。我将把它留给读者作为练习。