SwiftUI can't tap in Spacer of HStack

IosSwiftSwiftui

Ios Problem Overview


I've got a List view and each row of the list contains an HStack with some text view('s) and an image, like so:

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) }

This seems to work great, except when I tap in the empty section between my text and the image (where the Spacer() is) the tap action is not registered. The tap action will only occur when I tap on the text or on the image.

Has anyone else faced this issue / knows a workaround?

Ios Solutions


Solution 1 - Ios

As I've recently learned there is also:

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

Works well for me.

Solution 2 - Ios

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.

Solution 3 - Ios

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)
        }
    }

Solution 4 - Ios

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()
}

Solution 5 - Ios

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
}

Solution 6 - Ios

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())
    }

Solution 7 - Ios

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: {

})

Solution 8 - Ios

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
          }
        }
      }
    }
  }
}

Solution 9 - Ios

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.

Solution 10 - Ios

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.)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionQuinnView Question on Stackoverflow
Solution 1 - IoshnhView Answer on Stackoverflow
Solution 2 - Iosrob mayoffView Answer on Stackoverflow
Solution 3 - IosAsi GivatiView Answer on Stackoverflow
Solution 4 - IosJim MarquardtView Answer on Stackoverflow
Solution 5 - IosCasper ZandbergenView Answer on Stackoverflow
Solution 6 - IossergioblancooView Answer on Stackoverflow
Solution 7 - IosWilliam HuView Answer on Stackoverflow
Solution 8 - IosRufat MirzaView Answer on Stackoverflow
Solution 9 - IosarseniusView Answer on Stackoverflow
Solution 10 - IosMikael WeissView Answer on Stackoverflow