SwiftUI中的NavigationLink内部的按钮

19

我有一个用于显示任务基本信息的列表项视图,嵌入在一个导航链接中。

我想在导航链接中使用一个按钮来切换 task.isComplete 的状态,而不进行任何导航。

到目前为止,这是我的代码:

var body: some View {
        NavigationLink(destination: TaskDetailView()) {
            HStack {
                RoundedRectangle(cornerRadius: 5)
                    .frame(width: 15)
                    .foregroundColor(getColor(task: task))
                VStack {
                    Text(task.name!)
                        .font(.headline)
                    Spacer()
                }
                Spacer()
                Button(action: {
                    self.task.isComplete.toggle()
                }) {
                    if task.isComplete == true {
                        Image(systemName: "checkmark.circle.fill")
                    } else {
                        Image(systemName: "circle")
                    }
                }
                .foregroundColor(getColor(task: task))
                .font(.system(size: 22))
            }
        }
    }

目前,按钮操作不会执行,因为每当按下按钮时,导航链接都会将您带到目标视图。我尝试了将按钮放在导航链接外面 - 这样可以实现操作,但导航仍然会发生。

有没有一种方法能够实现这一点?

谢谢。

7个回答

17
为了达到你想要的结果,你需要使用其他东西来代替导航链接上的按钮,并在其中加入.onTapGesture处理程序。以下示例对我有效。
尝试替换:


尝试替换:
Button(action: {
    self.task.isComplete.toggle()
}) {
    if task.isComplete == true {
        Image(systemName: "checkmark.circle.fill")
    } else {
        Image(systemName: "circle")
    }
}

使用:

Image(systemName: task.isComplete ? "checkmark.circle.fill" : "circle")
    .onTapGesture { self.task.isComplete.toggle() }

3
简单而有效,这应该是最佳答案。 - Dirk Bester
2
这样做的一个缺点是,您不会自动获得按钮的按下状态(因为它不再是按钮)。 - sam-w
@sam-w,答案已经有1年了。谢谢。 - Alex Motor

13

我遇到了同样的问题,当我搜索解决方案时,这个问题基本上是排名第一的。因此,既然找到了一个相当优雅的解决方案,这似乎是一个好地方来分享。

问题似乎在于NavigationLink的手势安装方式使其忽略其子视图手势:它实际上使用了.gesture(_:including:)GestureMask.gesture,这意味着它将优先于任何类似优先级的内部手势。

最终,您需要的是一个带有.highPriorityGesture()的按钮来触发其操作,同时保持按钮的常规API:传递单个操作方法以在触发时运行,而不是定义一个简单的图像并摆弄手势识别器来更改状态等。要使用标准的Button行为和API,您需要深入了解一些按钮的行为,并直接定义其中一些行为。幸运的是,SwiftUI提供了适当的钩子来实现这一点。

有两种方法可以自定义按钮的外观:您可以实现ButtonStylePrimitiveButtonStyle,然后将其传递到.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()方法(前提是它仍然看起来像一个“轻触”)。然后,它返回按钮提供的标签(即原始Buttonlabel: {}参数的内容),附加了两个新的修饰符:一个.opacity(),其值为10.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”状态,只有在按下时结束才触发。我将把它留给读者作为练习。

5
.allowsHitTesting(false)添加到您的Button中,当用户点击它时,NavigationLink将被触发。
任何按钮状态,例如活动状态的样式,仍将起作用。
NavigationLink(destination: TheBestViewEver(), label: {
    
    Button("My Button", action: {
        
    })
    .allowsHitTesting(false)
    
})

可与Xcode 14.1、Swift 5、iOS 15.0+一起使用。也可能适用于旧版本。


这对我来说完美地起作用了,将来应该成为新的被接受的答案! - Stu P.

2
这个非常有效:
ZStack {
    NavigationLink(
        destination: DetailView(),
        isActive: $viewModel.show
    ) {
        Text("")
    }
    .hidden()
            
    Button {
        YourActionHere()
    } label: {
        Text("Button title here")
    }
}

2
尝试这种方法:
var body: some View {
        NavigationLink(destination: TaskDetailView()) {
            HStack {
                RoundedRectangle(cornerRadius: 5)
                    .frame(width: 15)
                    .foregroundColor(getColor(task: task))
                VStack {
                    Text(task.name!)
                        .font(.headline)
                    Spacer()
                }
                Spacer()
                Button(action: {}) {
                    if task.isComplete == true {
                        Image(systemName: "checkmark.circle.fill")
                    } else {
                        Image(systemName: "circle")
                    }
                }
                .foregroundColor(getColor(task: task))
                .font(.system(size: 22))
                .onTapGesture {
                  self.task.isComplete.toggle()
                }
            }
        }
    }

虽然现在可以按下按钮,但不幸的是每当按下按钮时,navigationLink也会被触发。 - Dylan Rowe
我没有看到你的所有代码,但是拦截TapGesture,如更新后,应该可以帮助你甚至在你的原始变体中。 - Asperi

1

我有同样的问题,这是我找到的答案:

所有的功劳都归给这个人和他的回答 https://dev59.com/5lMH5IYBdhLWcg3w1TuK#58176268

struct ScaleButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .scaleEffect(configuration.isPressed ? 2 : 1)
    }
}

struct Test2View: View {
    var body: some View {
        Button(action: {}) {
            Image("button1")
        }.buttonStyle(ScaleButtonStyle())
    }
}

似乎是.buttonStyle(ScaleButtonStyle())在此处发挥了作用。
这是我在项目中使用的方式,它很有效。
HStack {
    NavigationLink(destination: DetailView()) {
        VStack {
            Text(habits.items[index].name)
                .font(.headline)
            }
                    
        Spacer()
                    
                    
        Text("\(habits.items[index].amount)")
            .foregroundColor(.purple)
            .underline()
        }
                
        Divider()
                    
                
        Button(action: { habits.items[index].amount += 1 }) {
            Image(systemName: "plus.square")
                .resizable()
                .scaledToFit()
                .scaleEffect(self.scaleValue)
                .frame(height: 20)
                .foregroundColor(.blue)
        }.buttonStyle(ScaleButtonStyle())

                
}

-2
请将每个项目视图放在一个VStack中,而不是ListForm中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接