I have an HStack with buttons in it defined like this:
GeometryReader { proxy in
HStack {
Button("test")
.frame(width: 100, height: 50)
Button("test")
.frame(width: 100, height: 50)
Button("test")
.frame(width: 100, height: 50)
}
.frame(width: proxy.size.width)
.gesture(
.onChanged { gesture in
// do something here
}
)
}
As you can see, I am trying to perform some function when the HStack is dragged. This works if the user drags starting from an empty space in the HStack, however, if the drag starts from the button, the gesture is not activated. How can I fix this?
You can use a View with an onTapGesture instead of a button.
Try something like this:
#State private var count = 0 // To check if dragging works
GeometryReader { proxy in
HStack {
Text("test")
.onTapGesture { // The action gesture
//
}
.foregroundColor(.blue) // Colour it like the default button
.frame(width: 100, height: 50)
Text("\(count)")
}
.gesture(DragGesture(minimumDistance: 1) // The dragging gesture
.onChanged { gesture in
count += 1
})
}
You could possibly also try and disabling the button when dragging and then enable when you're done dragging.
Good luck!
I am building an app that has a helper modal, so a ? inside a circle is shown.
However the button appears to be too small to push, if I add a " ? " it seems to work but the button is offset.
Is there a way to make the hit box bigger?
HStack {
Text("Enter device ID. ").font(.custom("regular", size: 14)).foregroundColor(Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)))
.padding(.bottom, 8)
Button(action: {
self.isShowingModal = true;
}){
Text("?")
.foregroundColor(.blue)
.font(.custom("regular", size: 14))
.background(Circle().stroke(Color.blue).frame(width: 17, height: 17))
}
.padding(.bottom, 8)
.sheet(isPresented: $isShowingModal){
HelperModal()
}
}
If you need to expand the tap area of a Text, you can apply .frame modifier along with .contentShape.
Text("Example text")
.frame(width: frameWidth, height: frameHeight)
.contentShape(Rectangle()) // Choose a shape you'd like to use
I want to change the background color of my list when it is in .editMode. What I tried so far is the general approach to changing the .background of a list via the initialisation of the view the following way:
init() {
UITableView.appearance().backgroundColor = .clear
UITableView.appearance().separatorColor = .none
UITableView.appearance().separatorStyle = .none
}
and the setting the background color of the list with the .background modifier.
This is working, but once in .editMode the Color(.systemsBackground) (black) is shown instead of the my custom Color (
Picture of List in editMode).
My code for the list is:
List {
ForEach(categoryVM.selectedCategories) { category in
NavigationLink(destination: EditCategoryColorView(category: category, editType: .edit, name: category.name, color: category.color)){
ZStack {
HStack {
Text("\(category.name)")
Spacer()
Color("\(category.color)")
.aspectRatio(1, contentMode: .fit)
.cornerRadius(6)
.frame(width: 40, height: 40)
}
}
}.padding(3)
}.onDelete(perform: categoryVM.deleteCategory)
.onMove(perform: move)
.foregroundColor(colorScheme == .dark ? Color.white : Color.black)
.background(colorScheme == .dark ? Color("DarkGrayApp") : Color.white)
.listRow()
}
.padding(.vertical)
// to enable edit mode
EditButton()
In the code .listRow() is an extension, so the rows fill the whole space available.
extension View {
func listRow() -> some View {
self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1))
.background(Color(.systemBackground))
}
}
I tried changing the color in the extension as well but it did not work.
Any idea how I could change the background color once in .editMode as well?
Thank you!
Just an update: .listRowBackground(Color.clear) did the trick for me.
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
}
I have Button
struct ContentView : View {
var body: some View {
HStack {
Button(action: {}) {
Text("MyButton")
.color(.white)
.font(Font.system(size: 17))
}
.frame(height: 56)
.background(Color.red, cornerRadius: 0)
}
}
}
But I want to pin it to supreview's edges (trailing to superview's trailing and leading). Like this:
HStack doesn't help me, and it's expecting.
Fixed frame or width equals UIScree.size are not flexible solutions.
You need to use .frame(minWidth: 0, maxWidth: .infinity) modifier
Add the next code
Button(action: tap) {
Text("Button")
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.red)
}
.padding(.horizontal, 20)
Padding modifiers will allow you to have some space from the edge.
Keep in mind that the order of modifiers is essential. Because modifiers are functions that are wrapping the view below (they do not change properties of views)
You can use GeometryReader: https://developer.apple.com/documentation/swiftui/geometryreader
According to Apple:
This view returns a flexible preferred size to its parent layout.
It is a flexible solution, as it changes according to the parent layout changes.
Here's a pure SwiftUI solution where you want to fill the width of the parent but constrain the height to an arbitrary aspect ratio (say you want square images):
Image("myImage")
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(16.0 / 9.0, contentMode: .fit)
Just replace 16.0 / 9.0 with whatever aspect ratio you want.
I spent about a day trying to figure this out because I didn't want to use GeometryReader or UIScreen.main.bounds (which isn't cross-platform)
Edit: Found an even simpler way.
Simple adding following to your code will make the button extend edge to edge. (You can add padding as well if you want)
.frame(minWidth: 0, maxWidth: .infinity)
The entire Button code will look like this:
struct ContentView : View {
var body: some View {
HStack {
Button(action: {}) {
Text("MyButton")
.color(.white)
.font(Font.system(size: 17))
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.top, 8)
.padding(.bottom, 8)
.background(Color.red, cornerRadius: 0)
}
}
}
I found one solution:
var body: some View {
Button(action: {}) {
HStack {
Spacer()
Text("MyButton")
.color(.white)
.font(Font.system(size: 17))
Spacer()
}
.frame(height: 56)
.background(Color.red, cornerRadius: 0)
}.padding(20)
}
But I'm not sure that it is the best. May be someone will find more elegant solution/