163

The background area of my button is not detecting user interaction. Only way to interact with said button is to tap on the Text/ Label area of the button. How to make entire Button tappable?

struct ScheduleEditorButtonSwiftUIView: View {

    @Binding  var buttonTagForAction : ScheduleButtonType
    @Binding  var buttonTitle        : String
    @Binding  var buttonBackgroundColor : Color


    let buttonCornerRadius = CGFloat(12)

    var body: some View {

        Button(buttonTitle) {
              buttonActionForTag(self.buttonTagForAction)
        }.frame(minWidth: (UIScreen.main.bounds.size.width / 2) - 25, maxWidth: .infinity, minHeight: 44)

                            .buttonStyle(DefaultButtonStyle())
                            .lineLimit(2)
                            .multilineTextAlignment(.center)
                            .font(Font.subheadline.weight(.bold))
                            .foregroundColor(Color.white)

                            .border(Color("AppHighlightedColour"), width: 2)
                           .background(buttonBackgroundColor).opacity(0.8)

                            .tag(self.buttonTagForAction)
                            .padding([.leading,.trailing], 5)
       .cornerRadius(buttonCornerRadius)

    }
}
3
  • 28
    This is what you're looking for. I'd make an answer, but this explains in the best way possible: alejandromp.com/blog/2019/06/09/playing-with-swiftui-buttons Commented Aug 2, 2019 at 21:26
  • 1
    Excellent thank you that worked Commented Aug 2, 2019 at 22:04
  • Add some padding, and use contentShape Commented Dec 25, 2020 at 23:19

15 Answers 15

140

The proper solution is to use the .contentShape() API.

Button(action: action) {
  HStack {
    Spacer()
    Text("My button")
    Spacer()
  }
  .contentShape(Rectangle())
}

You can change the provided shape to match the shape of your button; if your button is a RoundedRectangle, you can provide that instead.

EDIT: as mentioned in the comments, .contentShape(Rectangle()) has been moved inside the button closure, otherwise it won't work on last versions of SwiftUI.

Sign up to request clarification or add additional context in comments.

7 Comments

Yes, this is the real answer, which works whether or not you have a background color.
Adding .contentShape(Rectangle()) to HStack inside button worked for me
This is the only solution that worked for me, except I had to put it on the stack inside the button, not on the button itself.
If you use a custom ButtonStyle, then other solutions don't work. This is the true solution to the issue.
@EugeneStrelnikov's edit to move the contentShape to the hstack worked for me. perfect.
|
102

I think this is a better solution, add the .frame values to the Text() and the button will cover the whole area 😉

Button(action: {
    //code
}) {
    Text("Click Me")
    .frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .center)
    .foregroundColor(Color.white)
    .background(Color.accentColor)
    .cornerRadius(7)
}

2 Comments

Great fix that makes sense.
this is not working on Xcode 11.5 beta. I tried two approaches, the one in the answer and also moving the size to a custom ButtonStyle
43

You can define content Shape for hit testing by adding modifier: contentShape(_:eoFill:)

And important thing is you have to apply inside the content of Button.

Button(action: {}) {
    Text("Select file")
       .frame(width: 300)
       .padding(100.0)
       .foregroundColor(Color.black)
       .contentShape(Rectangle()) // Add this line
}
.background(Color.green)
.cornerRadius(4)
.buttonStyle(PlainButtonStyle())

Another

Button(action: {}) {
    VStack {
       Text("Select file")
           .frame(width: 100)
       Text("Select file")
           .frame(width: 200)
    }
    .contentShape(Rectangle()) // Add this inside Button.
}
.background(Color.green)
.cornerRadius(4)
.buttonStyle(PlainButtonStyle())

1 Comment

this idea to use .contentShape(Rectangle()) fixed it for me
25

This fixes the issue on my end:

var body: some View {
    GeometryReader { geometry in
        Button(action: {
            // Action
        }) {
            Text("Button Title")
                .frame(
                    minWidth: (geometry.size.width / 2) - 25,
                    maxWidth: .infinity, minHeight: 44
                )
                .font(Font.subheadline.weight(.bold))
                .background(Color.yellow).opacity(0.8)
                .foregroundColor(Color.white)
                .cornerRadius(12)

        }
        .lineLimit(2)
        .multilineTextAlignment(.center)
        .padding([.leading,.trailing], 5)
    }
}

button

Is there a reason why you are using UIScreen instead of GeometryReader?

2 Comments

Did not know about GeometryReader. Thanks for the info I shall read up on it
Great answer. I had the size modifier on the button itself, and not the text :)
24

Short Answer

Make sure the Text (or button content) spans the length of the touch area, AND use .contentShape(Rectangle()).

Button(action:{}) {
  HStack {
    Text("Hello")
    Spacer()
  }
  .contentShape(Rectangle())
}

Long Answer

There are two parts:

  1. The content (ex. Text) of the Button needs to be stretched
  2. The content needs to be considered for hit testing

To stretch the content (ex. Text):

// Solution 1 for stretching content
HStack {
  Text("Hello")
  Spacer()
}

// Solution 2 for stretching content
Text("Hello")
  .frame(maxWidth: .infinity, alignment: .leading)

// Alternatively, you could specify a specific frame for the button.

To consider content for hit testing use .contentShape(Rectangle()):

// Solution 1
Button(action:{}) {
  HStack {
    Text("Hello")
    Spacer()
  }
  .contentShape(Rectangle())
}

// Solution 2
Button(action:{}) {
  Text("Hello")
    .frame(maxWidth: .infinity, alignment: .leading)
    .contentShape(Rectangle())
}

Comments

5

You might be doing this:

Button { /*to do something on button click*/} 
label: { Text("button text").foregroundColor(Color.white)}
.frame(width: 45, height: 45, alignment: .center)
.background(Color.black)

Solution:

Button(action: {/*to do something on button click*/ }) 
{ 
HStack { 
Spacer()
Text("Buttton Text")
Spacer() } }
.frame(width: 45, height: 45, alignment: .center)
.foregroundColor(Color.white)
.background(Color.black).contentShape(Rectangle())

Comments

3

I was working with buttons and texts that need user interaction when I faced this same issue. After looking and testing many answers (including some from this post) I ended up making it works in the following way:

For buttons:

/* WITH IMAGE */
Button {
    print("TAppeD")
} label: {
    Image(systemName: "plus")
        .frame(width: 40, height: 40)
}
/* WITH TEXT */
Button {
    print("TAppeD")
} label: {
    Text("My button")
       .frame(height: 80)
}
 

For Texts:

Text("PP")
    .frame(width: 40, height: 40)
    .contentShape(Rectangle())
    .onTapGesture {
        print("TAppeD")
    }

In the case of the texts, I only need the .contentShape(Rectangle()) modifier when the Text doesn't have a .background in order to make the entire Text frame responsive to tap gesture, while with buttons I use my Text or Image view with a frame and neither a .background nor a .contentShape is needed.

Image of the following code in preview (I'm not allowed to include pictures yet )

import SwiftUI

struct ContentView: View {
    @State var tapped: Bool = true
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 19)
                .frame(width: 40, height: 40)
                .foregroundColor(tapped ? .red : .green)
            Spacer()
            HStack (spacing: 0) {
                Text("PP")
                    .frame(width: 40, height: 40)
                    .contentShape(Rectangle())
                    .onTapGesture {
                        tapped.toggle()
                    }
                
                Button {
                    print("TAppeD")
                    tapped.toggle()
                } label: {
                    Image(systemName: "plus")
                        .frame(width: 40, height: 40)
                }
                .background(Color.red)
                
                Button {
                    print("TAppeD")
                    tapped.toggle()
                } label: {
                    Text("My button")
                        .frame(height: 80)
                }
                .background(Color.yellow)
            }
            
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Comments

2

A bit late to the answer, but I found two ways to do this —

Option 1: Using Geometry Reader

Button(action: {
}) {
    GeometryReader { geometryProxy in
        Text("Button Title")
            .font(Font.custom("SFProDisplay-Semibold", size: 19))
            .foregroundColor(Color.white)
            .frame(width: geometryProxy.size.width - 20 * 2) // horizontal margin
            .padding([.top, .bottom], 10) // vertical padding
            .background(Color.yellow)
            .cornerRadius(6)
    }
}

Option 2: Using HStack with Spacers

HStack {
    Spacer(minLength: 20) // horizontal margin
    Button(action: {

    }) {
        Text("Hello World")
            .font(Font.custom("SFProDisplay-Semibold", size: 19))
            .frame(maxWidth:.infinity)
            .padding([.top, .bottom], 10) // vertical padding
            .background(Color.yellow)
            .foregroundColor(Color.white)
            .cornerRadius(6)
    }
    Spacer(minLength: 20)
}.frame(maxWidth:.infinity)

My thought process here is that although option 1 is more succinct, I would choose option 2 since it's less coupled to its parent's size (through GeometryReader) and more in line of how I think SwiftUI is meant to use HStack, VStack, etc.

2 Comments

How dou sing the surrounding spacer effect that the whole button is sensitive to a tap event and not only it's text content?
For me second option it is better.
1

This one worked for me, just add text and give it dimensions as you require.

Button(action: {
            //action here...
        }){
            Text("Continue") // add this Text
                .frame(width:350, height: 60) // your required dimensions
        }
        .foregroundColor(.white)
      //  .frame(width:300, height: 60) this does not work as it does not increase text's dimension

        .background(
            .black
        )
        .padding()

Comments

0

this way makes the button area expand properly but if the color is .clear, it dosen't work🤷‍♂️

                Button(action: {
                   doSomething()
                }, label: {
                    ZStack {
                        Color(.white)
                        Text("some texts")
                    }
                })

Comments

0

When I used HStack then it worked for button whole width that's fine, But I was facing issue with whole button height tap not working at corners and I fixed it in below code:

Button(action:{
                print("Tapped Button")
            }) {
                VStack {
                   //Vertical whole area covered
                    Text("")
                    Spacer()
                    HStack {
                     //Horizontal whole area covered
                        Text("")
                        Spacer()
                    }
                }
            }

Comments

0

If your app needs to support both iOS/iPadOS and macOS, you may want to reference my code!

Xcode 14.1 / iOS 14.1 / macOS 13.0 / 12-09-2022

Button(action: {
    print("Saved to CoreData")
    
}) {
    Text("Submit")
    .frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 60, alignment: .center)
    .foregroundColor(Color.white)
    #if !os(macOS)
    .background(Color.accentColor)
    #endif
}
#if os(macOS)
.background(Color.accentColor)
#endif
.cornerRadius(7)

Comments

0

Easier work around is to add .frame(maxWidth: .infinity) modifier. and wrap your button inside a ContainerView. you can always change the size of the button where it's being used.

    Button(action: tapped) {
        HStack {
            if let icon = icon {
                icon
            }

            Text(title)
        }
        .frame(maxWidth: .infinity) // This one
    }

Comments

0

These are behaviours with SwiftUI which usually surprise developers:

  • When you modify a button with .frame, it changes the container, but the button size didn't change. That's because the button label is fixed size.
  • When you subsequently modify the frame with .contentShape, it doesn't change the button hittable area as you are modifying the frame

The initialization using Button(title) uses a fixed size label, which you can't do much about. The solution is to create the label view yourself.

Button {
  // action
} label: {
  Text(title)
    .frame(minWidth: 100, minHeight: 44)
}

Comments

0

This works neatly for me, no need for geometry readers or infinite widths:

Button(action: {
    print("tap")
}, label: {
    HStack(spacing: 16) {
        Spacer()
        Text("Foo")
            .foregroundStyle(Color.white)
            .font(.bodyMedium)
        Image("Bar", bundle: Bundle.module)
        Spacer()
    }
    .padding(12)
    .background(Color.blue)
    .cornerRadius(8)
})

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.