SwiftUI. Square shadow around a circular image when using the context menu - swiftui

In my application, I am using a circular image with a drop shadow as a context menu button.
Code:
Image(systemName: "plus")
.padding(20)
.background(Color.yellow)
.clipShape(Circle())
.shadow(radius: 10)
.contextMenu{
Button {} label: {
Label("test", systemImage: "")
}
}
Result:
When the context menu is invoked, the shadow becomes square ((
Can I fix this somehow?

The solution is here: SwiftUI Image clipped to shape has transparent padding in context menu . In my case I need to add the modifier .contentShape(Circle()) to Image().
Final correct code:
Image(systemName: "plus")
.padding(20)
.background(Color.yellow)
.clipShape(Circle())
.shadow(radius: 10)
.contentShape(Circle()) //here
.contextMenu {
Button {} label: {
Label("test", systemImage: "")
}
}

Related

SwiftUI: add border to view without bleeding out?

I have a simple view like this:
struct TestView: View {
var body: some View {
Button(action: {}) {
Text("Button")
.padding()
.foregroundColor(.white)
}
.background(Color(.gray))
// .cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.red, lineWidth:4)
)
}
}
It draws this:
I am trying to understand why the border bleeds out of the view (gray area). I want to draw the border so it stays inside the view (in the gray area). It looks like half of its width is out and Half in.
How can I make the border stay inside the overlay bounds?
How can I make the border stay inside the overlay bounds?
Use strokeBorder(_:lineWidth:antialiased:) instead β€” this draws an inner stroke.
Button(action: {}) {
Text("Button")
.padding()
.foregroundColor(.white)
}
.background(Color(.gray))
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(.red, lineWidth: 4) /// here!
)
If you want it to look like this:
Button(action: {}) {
Text("Button")
.padding()
.foregroundColor(.white)
}
.background(Color(.gray))
.clipShape(RoundedRectangle(cornerRadius:10))
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(.red, lineWidth: 4) /// here!
)

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

SwiftUI, remove space between views in a VStack?

Why is there so much space between the three blue rectangles and the list? How can I remove the space so that all views within the VStack stack at the top? I tried using a Spacer() directly after the List, but nothing changed.
struct ContentView: View {
init() { UITableView.appearance().backgroundColor = UIColor.clear }
var body: some View {
NavigationView {
ZStack {
Color.red
.ignoresSafeArea()
VStack {
HStack {
Text("Faux Title")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.heavy)
Spacer()
Button(action: {
// settings
}, label: {
Image(systemName: "gearshape.fill")
.font(.system(.title2))
})
}
.padding()
GeometryReader { geometry in
HStack() {
Text("1")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
Spacer()
Text("2")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
Spacer()
Text("3")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
}
}
.padding()
List {
Text("One")
Text("Two")
Text("Three")
Text("Four")
Text("Five")
Text("Six")
}
.listStyle(InsetGroupedListStyle())
}
}
.navigationBarHidden(true)
}
}
}
Bonus question: In web development, you can open your browser's Web Inspector and use the element selector to click on elements which highlights their borders. Useful for something like this where you're trying to figure out which element the offending spacing belongs to. Is there something like that in Xcode?
VStack(spacing: 0) {...}
Spacer()
to your question you can in Xcode use the view inspector. https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/ExaminingtheViewHierarchy.html
Since you know that your HStack with the blue rectangles is going to be a height of 150, you should constrain it to that using .frame(height: 150):
GeometryReader { geometry in
...
}
.padding()
.frame(height: 150) //Here
Otherwise, the GeometryReader will occupy all available vertical space.
Re: your web dev comparison, check out the Xcode view hierarchy inspector. It's not exactly the same, but it's in the same vein: https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/ExaminingtheViewHierarchy.html

Spacing Issue with SwiftUI

I am experiencing a weird spacing behavior that I'm hoping someone can explain. I have two views, the main view (ContentView) contains a child view called PlayerToolbar. The desired behavior is for ContentView take up the entire screen with PlayerToolbar being rendered at the very bottom of the screen. PlayerToolbar contains image buttons and spacers. The issue I am running into is ContentView only takes up a portion of the screen and PlayerToolbar is not aligned to the bottom as shown in the image.
Here is the code for ContentView
struct ContentView: View {
var body: some View {
VStack{
Spacer()
Text("Main Content")
Spacer()
PlayerToolBar()
}.background(Color.blue)
}
}
And here is the code for PlayerToolbar:
struct PlayerToolBar: View {
var body: some View {
HStack{
Spacer()
Button(action: {
print("backward button pressed")
}){
Image(systemName: "gobackward.10").renderingMode(.original) .resizable().aspectRatio(contentMode: .fit)
}
Spacer()
Button(action: {
print("play button pressed")
}){
Image(systemName: "play.circle").renderingMode(.original) .resizable().aspectRatio(contentMode: .fit)
}
Spacer()
Button(action: {
print("go forward button pressed")
}){
Image(systemName: "goforward.10").renderingMode(.original) .resizable().aspectRatio(contentMode: .fit)
}
Spacer()
Button(action: {
print("jot button pressed")
}){
Image(systemName: "pencil.circle").renderingMode(.original) .resizable().aspectRatio(contentMode: .fit)
}
Spacer()
}.background(Color(UIColor.secondarySystemBackground))
}
}
I have found that if add one Text object in my PlayerToolbar between the first Spacer and Button, the screen renders as I expect
...
Spacer()
Text(" ")
Button(action: {
print("backward button pressed")
}){
Image(systemName: "gobackward.10").renderingMode(.original) .resizable().aspectRatio(contentMode: .fit)
}
...
Any idea of why it is behaving the way it is and why a Text makes it act the way I prefer?
The problem, I think, is because your PlayerToolbar has no height and it is hard for the layout logic to determine one. Nothing in your PlayerToolbar has an explicit height. Your images are made resizable, but nothing in your views is telling how to resize them.
By adding a Text() view, your images now have some height to match, and so it works as you expect it.
Other solutions to break the ambiguity are (choose any, not all):
Set a frame height to the PlayerToolbar:
PlayerToolBar().frame(height: 40)
Set the height for at least one of your images:
Image(systemName: "gobackward.10")
.renderingMode(.original)
.resizable()
.frame(height: 40)
.aspectRatio(contentMode: .fit)
Set the height to one of your buttons:
Button(action: {
print("backward button pressed")
}){
Image(systemName: "gobackward.10")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
}.frame(height: 40)
Set the height of the HStack in your PlayerToolbar.
Remove the resizable() modifier in at least one of your images.
Image(systemName: "pencil.circle").renderingMode(.original).aspectRatio(contentMode: .fit)
All these alternatives aim at the same thing, making sure your images know how much to grow/shrink. There are of course many other options. These are just a few.

SwiftUI Button tap only on text portion

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
}