SwiftUI 不能点击 HStack 的 Spacer

我有一个 List 视图,列表的每一行都包含一个 HStack,其中包含一些文本视图和一个图像,如下所示:

HStack{
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}.tapAction { self.groupSelected(self.group) }

这似乎工作得很好,除了当我点击我的文本和图像之间的空白部分(其中的 Spacer()是)点击动作没有注册。只有当我点击文本或图像时,点击操作才会发生。

有没有其他人遇到过这个问题/知道解决办法?

18577 次浏览

I've filed feedback on this, and suggest you do so as well.

In the meantime an opaque Color should work just as well as Spacer. You will have to match the background color unfortunately, and this assumes you have nothing to display behind the button.

Why not just use a Button?

Button(action: { self.groupSelected(self.group) }) {
HStack {
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}
}.foregroundColor(.primary)

If you don't want the button to apply the accent color to the Text(group.name), you have to set the foregroundColor as I did in my example.

I've been able to work around this by wrapping the Spacer in a ZStack and adding a solid color with a very low opacity:

ZStack {
Color.black.opacity(0.001)
Spacer()
}

As I've recently learned there is also:

HStack {
...
}
.contentShape(Rectangle())
.onTapGesture { ... }

Works well for me.

Simple extension based on Jim's answer

extension Spacer {
/// https://stackoverflow.com/a/57416760/3393964
public func onTapGesture(count: Int = 1, perform action: @escaping () -> Void) -> some View {
ZStack {
Color.black.opacity(0.001).onTapGesture(count: count, perform: action)
self
}
}
}

Now this works

Spacer().onTapGesture {
// do something
}

I just add the background color(except clear color) for HStack works.

HStack {
Text("1")
Spacer()
Text("1")
}.background(Color.white)
.onTapGesture(count: 1, perform: {


})

works like magic on every view:

extension View {
func onTapGestureForced(count: Int = 1, perform action: @escaping () -> Void) -> some View {
self
.contentShape(Rectangle())
.onTapGesture(count:count, perform:action)
}
}

Although the accepted answer allows the mimicking the button functionality, visually it does not satisfy. Do not substitute a Button with a .onTapGesture or UITapGestureRecognizer unless all you need is an area which accepts finger tap events. Such solutions are considered hacky and are not good programming practices.

To solve your problem you need to implement the BorderlessButtonStyle ⚠️

Example

Create a generic cell, e.g. SettingsNavigationCell.

SettingsNavigationCell

struct SettingsNavigationCell: View {
  

var title: String
var imageName: String
let callback: (() -> Void)?


var body: some View {
    

Button(action: {
callback?()
}, label: {


HStack {
Image(systemName: imageName)
.font(.headline)
.frame(width: 20)
        

Text(title)
.font(.body)
.padding(.leading, 10)
.foregroundColor(.black)
        

Spacer()
        

Image(systemName: "chevron.right")
.font(.headline)
.foregroundColor(.gray)
}
})
.buttonStyle(BorderlessButtonStyle()) // <<< This is what you need ⚠️
}
}

SettingsView

struct SettingsView: View {
  

var body: some View {
    

NavigationView {
List {
Section(header: "Appearance".text) {
          

SettingsNavigationCell(title: "Themes", imageName: "sparkles") {
openThemesSettings()
}
          

SettingsNavigationCell(title: "Lorem Ipsum", imageName: "star.fill") {
// Your function
}
}
}
}
}
}

Kinda in the spirit of everything that has been said:

struct NoButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(Color.black.opacity(0.0001))
}
}
extension View {
func wrapInButton(action: @escaping () -> Void) -> some View {
Button(action: action, label: {
self
})
.buttonStyle(NoButtonStyle())
}
}

I created the NoButtonStyle because the BorderlessButtonStyle was still giving an animation that was different than .onTapGesture

Example:

HStack {
Text(title)
Spacer()
Text("Select Value")
Image(systemName: "arrowtriangle.down.square.fill")
}
.wrapInButton {
isShowingSelectionSheet = true
}

Another option:

extension Spacer {
func tappable() -> some View {
Color.blue.opacity(0.0001)
}
}

Updated:

I've noticed that Color doesn't always act the same as a Spacer when put in a stack, so I would suggest not using that Spacer extension unless you're aware of those differences. (A spacer pushes in the single direction of the stack (if in a VStack, it pushes vertically, if in a HStack, it pushes out horizontally, whereas a Color view pushes out in all directions.)

The best approach in my opinion for accessibility reasons is to wrap the HStack inside of a Button label, and in order to solve the issue with Spacer can't be tap, you can add a .contentShape(Rectangle()) to the HStack.

So based on your code will be:

    Button {
self.groupSelected(self.group)
} label: {
HStack {
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}
.contentShape(Rectangle())
}

This website helped answer the same question for me: https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-the-tappable-area-of-a-view-using-contentshape

Try applying .ContentShape(Rectangle()) to the HStack.