SwiftUI Button tap only on text portion - swiftui

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

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.

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

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

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)
}
}
Is there a reason why you are using UIScreen instead of GeometryReader?

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:
The content (ex. Text) of the Button needs to be stretched
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())
}

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

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.

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

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

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

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)

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
}

Related

tab bar customisation swift ui

i am new in swiftUI and i want to make custom tab bar.
Let's ignore effects and icons.
I create new struct struct BottomPoligon: Shape for draw this Shape and after this i create new view TabBarView:
struct TabBarView: View {
var body: some View {
Spacer()
Button {
print("tap")
} label: {
ZStack {
Circle()
.fill(.clear)
.border(.red, width: 2)
Image(systemName: "barcode.viewfinder")
}
}
.frame(width: 50, height: 50)
HStack(alignment: VerticalAlignment.center) {
Button {
print("tap")
} label: {
Image(systemName: "person")
}
.frame(maxWidth: .infinity)
Button {
print("tap")
} label: {
Image(systemName: "list.bullet.circle.fill")
}
.frame(maxWidth: .infinity)
}
.frame(height: 50)
.background(
BottomPoligon(height: 50, cornerRadius: 15)
// .fill(.gray)
.stroke(Color.gray)
)
}
}
But I don't know how to position the button in the middle. Do you have any idea how I can make this or maybe another approach to make this? Is my overall idea good or bad?

Limit the number of dots in Page Tab View Swiftui

I am trying to use the PageTabview option to allow a user to move through a series of pages whose data is coming from a JSON file. I want to be able to limit the number of visible dots to 5 or 6 even if there are many values in the field. What I don't want is to have 25 dots if there are twenty-five values in the field. How would I do that? I want to be able to show indicator like an arrow that tells the user there is more to come...Thank you.
My code is below:
struct TopicsExperienceCards: View {
#Binding var closeExperience: Bool
let etype: EItype
var body: some View {
//start of content of zstack layout
ZStack {
VStack(spacing: 20) {
HStack{
Rectangle()
.fill(Color.white)
.frame(width: 300, height: 1, alignment: .center)
Spacer()
Button(action: {
closeExperience = false })
{
Image(systemName:"xmark")
.foregroundColor(Color(etype.accentcolor))
.padding()
}
} //HSTACK
TabView {
ForEach(etype.experience,id: \.self) { item in
// Display the content of a card //
VStack (alignment:.center, spacing:0){
Text(item)
.padding()
.frame(width:300, height:300, alignment:.center)
Divider()
Spacer()
Text("Room for an image")
Spacer()
Spacer()
}//VSTACK
//End of display of content of the card //
} //: FOREACH
} //: TABVIEW
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onAppear {
setupAppearance() }
} //VSTACK
} //: ZStack
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 16)
.strokeBorder()
.foregroundColor(Color(etype.accentcolor)))
.cornerRadius(16.0)
.padding()
}
}
struct TopicsExperienceCards_Previews: PreviewProvider {
static let etypes: [EItype] = Bundle.main.decode("eibasestructure.json")
static var previews: some View {
TopicsExperienceCards(closeExperience:.constant(true),etype:etypes[1])
}
}
enter image description here
System dots view is limited to around 10 dots, maybe depending on the device. You can't change this value.
Instead of that you can hide system one, and create your own view with dots. As an example you can follow this article, so at the end you'll have something like this:
#State var currentIndex = 0
var body: some View {
//start of content of zstack layout
ZStack {
printUI(currentIndex)
VStack(spacing: 20) {
HStack{
Rectangle()
.fill(Color.white)
.frame(width: 300, height: 1, alignment: .center)
Spacer()
Button(action: {
closeExperience = false })
{
Image(systemName:"xmark")
.foregroundColor(Color.red)
.padding()
}
} //HSTACK
TabView(selection: $currentIndex.animation()) {
ForEach(etype.experience.indices,id: \.self) { i in
let item = etype.experience[i]
// Display the content of a card //
VStack (alignment:.center, spacing:0){
Text(item)
.padding()
.frame(width:300, height:300, alignment:.center)
Divider()
Spacer()
Text("Room for an image")
Spacer()
Spacer()
}//VSTACK
//End of display of content of the card //
} //: FOREACH
} //: TABVIEW
.tabViewStyle(PageTabViewStyle())
.onAppear {
setupAppearance()
}
Fancy3DotsIndexView(numberOfPages: etype.experience.count, currentIndex: currentIndex)
.padding()
.background(Color.green)
.clipShape(Capsule())
} //VSTACK
} //: ZStack
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 16)
.strokeBorder()
.foregroundColor(Color.red)
)
.cornerRadius(16.0)
.padding()
}
Result:

How to increase the size of a Button in SwiftUI? (Multiplatform project)

I am trying to increase the size oaf a Button in SwiftUI, Xcode 12.5 in a Multiplatform project:
Button("Click me") {
// Perform action here
}
.frame(width: 100, height: 100)
.background(Color.yellow)
.buttonStyle(BorderlessButtonStyle())
After checking the API I found that I can style the button but I can not figure out how to make the click box larger. So that the entire yellow frame receives the click action not just the label.
Edit:
The The tap area problem is explained well here: https://alejandromp.com/blog/playing-with-swiftui-buttons/
But the solution there to change add an element to the button and change the size of that does only work in an iOS project. In a Multiplatform project the frame of the button has the wrong size:
Button(action: {}, label: {
Text("Click me")
.frame(width: 100, height: 100)
.background(Color.yellow)
})
Here is a possible solution
For iOS
Button(action: {
//add actions at here
}) {
VStack {
Text("Name")
}.frame(width: 100, height: 100)
.background(Color.yellow)
.cornerRadius(20)
}
For macOS (macOS version works well for iOS version too)
struct ContentView: View {
var body: some View {
VStack{
Button(action: {
//add actions at here
}) {
VStack {
Text("Button Name")
}.frame(width: 100, height: 100)
.background(Color.yellow)
.cornerRadius(20)
}.buttonStyle(CustomButtonStyle())
}
}
}
struct CustomButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(Color.blue)
.cornerRadius(10.0)
.padding()
}
}
I have found this solution where adding contentShape seems to do the trick:
Button(action: doSomething) {
Text("Click me")
.frame(width: 100, height: 100)
.contentShape(Rectangle())
}
.background(Color.yellow)
.buttonStyle(PlainButtonStyle())
The button cannot recognize the modifiers on that place. The solution would be:
Button(action: {}, label: {
Text("Click me")
.frame(width: 100, height: 100)
.background(Color.yellow)
})

Button with image for SwiftUI 2

I want to add button with image inside of the view like twitter button with image for SwiftUI 2, but it is not padding to any directions.
struct WeatherView: View {
var body: some View {
VStack(alignment: .leading, spacing:0){
Button(action: {}) {
Image("cloud")
.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.clipShape(Circle())
.shadow(radius: 8)
}
HStack(spacing: 5){
Text("Rainy")
.font(.largeTitle)
.fontWeight(.medium)
Spacer()
Button(action: {
}) {
Image("menu").resizable().frame(width: 24, height: 24)
}
}
.foregroundColor(Color("black"))
.padding()
Spacer()
}
}
}
Is there any solution for this problem.
I hope you are doing well.
This code should do the trick.
VStack {
Spacer()
Button(action: {}) {
Image(systemName: "cloud.fill")
.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.clipShape(Circle())
.shadow(radius: 8)
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
By putting the button in a VStack, and putting a spacer before it, you will push the button to the bottom of the screen. Then by putting a frame on it, and making the width to be .infinity, and giving it alignment trailing, you will further push the button to the trailing side of the screen, which in this case results in making the button at the bottom right corner.
Edit: Included full code
struct WeatherView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 5) {
Text("Rainy")
.foregroundColor(.black)
.font(.largeTitle)
.fontWeight(.medium)
Spacer()
Button(action: {}) {
Image("menu").resizable().frame(width: 24, height: 24)
}
}
.foregroundColor(Color("black"))
.padding()
VStack {
Spacer()
Button(action: {}) {
Image("cloud")
.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.clipShape(Circle())
.shadow(radius: 8)
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
}
}
}

how to strech image on one side in swiftUI, like a .9 file in Android?

i have an image that i want to strech only it's height to fit different content, how do i do that in swiftUI? right now it looks like this
struct ContentView: View {
var body: some View {
VStack {
HStack {
VStack(alignment: .leading, spacing: 130) {
Text("Title")
.font(.headline)
.foregroundColor(.primary)
Text("text")
.font(.subheadline)
.foregroundColor(.secondary)
Text("padding")
}
.padding(.vertical)
Spacer()
Image("rightTag")
.resizable(capInsets: .init(top: 0, leading: 0, bottom: 0, trailing: 0), resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 20)
}
.frame(maxWidth: screen.width - 60)
.padding(.leading)
.background(.white)
.cornerRadius(20)
}
}
}
how can i stretch its height to fit this outer frame? ragular resizable and stuff can't get it done.
any helped would be wonderful! Thanks!
sry i didn't make myself clear earllier.
Here is possible approach. However as I see now you'd rather need to stretch not the entire original image, but only middle of it, so in real project it would be needed to make your image tri-part and apply below stretching approach only to middle (square) part.
Approach uses asynchronous state update, so works in Live Preview / Simulator / RealDevice. (Tested with Xcode 11.2 / iOS 13.2)
struct ContentView: View {
#State private var textHeigh: CGFloat = .zero
var body: some View {
VStack {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 130) {
Text("Title")
.font(.headline)
.foregroundColor(.primary)
Text("text")
.font(.subheadline)
.foregroundColor(.secondary)
Text("padding")
}
.padding(.vertical)
.alignmentGuide(.top, computeValue: { d in
DispatchQueue.main.async {
self.textHeigh = d.height // !! detectable height of left side text
}
return d[.top]
})
Spacer()
Image("rightTag")
.resizable()
.frame(maxWidth: 20, maxHeight: max(60, textHeigh)) // 60 just for default
}
.frame(maxWidth: screen.width - 60)
.padding(.leading)
.background(Color.white)
}
}
}