SwiftUI .contextMenu has visible padding around my View with a clipShape - ios

I have an image in a chat bubble. This effect is achieved using a clipShape to a custom shape for the bubble.
When I attach a .contextMenu and present the contextMenu, it shows a padding around the chat bubble. Also the default cornerRadius of the contextMenu clips a small part of the chat bubble tip.
What I am trying to achieve is what I can see in the Apple Messages app. presenting contextMenu on a message with text preserves the clipShape of the text without adding padding. And on an image it removes the clipShape and resizes the image to a proper aspect Ratio. Neither of which I'm able to achieve.
struct ContentView: View {
var body: some View {
VStack {
Image("leaf")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 264, height: 361)
.clipShape(Bubble(location: .rightBottom))
.contentShape(Bubble(location: .rightBottom))
.contextMenu(ContextMenu(menuItems: {
Button {
} label: {
HStack {
Text("Save")
Image(systemName: "arrow.down.to.line")
}
}
}))
}
}
}
I have tried to use .contentShape to the same shape of the bubble but that had no effect at all. Is there anyway to show the contextMenu without the rounded corners and extra padding on the left side in screenshot below? Or to make the contextMenu containers background clear ideally?
Image prior to pressing context menu:

The solution was very simple. Just use content shape's first argument kind:
func contentShape<S>(_ kind: ContentShapeKinds, _ shape: S)
and set kind to .contextMenuPreview:
Image("leaf")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 264, height: 361)
.clipShape(Bubble(location: .rightBottom))
.contentShape(.contextMenuPreview, Bubble(location: .rightBottom))
.contextMenu(ContextMenu(menuItems: {
Button {
} label: {
HStack {
Text("Save")
Image(systemName: "arrow.down.to.line")
}
}
}))

Related

SwiftUI - can I make one element in Form fill the whole screen width (without horizontal margins)?

I would like a single item inside SwiftUI Form to run from side to side, without having Form's default margins.
Unfortunately, whatever I do (like ading a wider .frame, negative horizontal padding, or .offset), the team image view seems to be always cropped by the form to fit the form area (= has horizontal margins).
Is it possible to make the Image below touch the left and right side of the screen?
I am using Form for my app settings, but I would like to add a full-width section there (think eg. a banner to promote a feature).
SwiftUI Playgrounds code:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View {
Form {
Section(
header: Text("First section")
) {
Text("Hello world")
}
Text("The image below should be stretched to touch the left and right window edge, without being cropped by the Form.")
Image(systemName: "sun.max.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.listRowInsets(EdgeInsets()) // this is supposed to fix the problem, but all it does is to set the default item inner padding to zero, so the image at least touches the edge of teal area.
.listRowBackground(Color.teal)
Section(
header: Text("Last section")
) {
Text("Hello world")
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
How it looks:
Unfortunately, SwiftUI Form is very temperamental and forces you to strictly adhere to the standard iOS Settings screen formatting.
Fortunately, you can re-implement similar formatting yourself with SwiftUI!
For the top, something like:
VStack(spacing: 4) {
Text("FIRST SECTION")
.font(.system(size: 12, weight: .regular))
.foregroundColor(.gray)
.padding(.leading)
Text("Hello, world!")
.font(.system(size: 15, weight: .regular))
.foregroundColor(.black)
.padding(.horizontal)
.frame(height: 44, alignment: .center)
.background(Color.white)
.cornerRadius(10)
}

Background Img not staying on Screen Preview (SwiftUI w/ screen shots)

Xcode Version - 13.1
I'm having some issues with a Background Img & the grouping Form{}. What's happening is when placing a background image in a ZStack and include the grouping option, Form{}, the Background Img disappears.
Below my code showing my Background Image inside a ZStack. Also including a link to a screen shot of the Preview -> https://i.stack.imgur.com/u12Sw.png
var body: some View {
ZStack {
Image("login")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
}
}
However, when I add a Form{} inside the ZStack, the background image completely disappears and only the Form{} (w/ a TextField &SecureTextField) appears on the Preview Sans the Background Img. Below is the code w/ and a link to the Screen shot of the Preview -> https://i.stack.imgur.com/BI3U2.png
var body: some View {
ZStack {
Image("login")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
Form {
TextField(
"Username (email)",
text: self.$username)
.autocapitalization(.none)
.disableAutocorrection(true)
SecureTextField(text: $password)
}
}
}
My assumption is that as long as the Form is inside the ZStack, the form should overlay the Background Img.
I like how the Form looks rather than two separate TextFields. Are there anyways to do this w/ the Form or something similar to a Form?
This is what I do at the moment:
I either add this modifier to the form.
.onAppear {
UITableView.appearance().backgroundColor = .clear
}
Or in an init() of the View
this has some side effects on the rest of the view if you have List or Forms but it works for me.

How to get the size of the corner radius of widget in SwiftUI?

I want to maintain the same inset around my custom view. Exterior corner radius is required to change the corner radius of our custom views,
I could not find any environment variable or any other way to get the corner radius of the widget itself.
Just parsing a hardCoded value to .cornerRadius() is not work on every device. Since
different devices may use a different radius for their widgets.
var body: some View {
VStack {
HStack {
VStack {
Text("Hello")
.font(.title)
Text("Widget")
}
.padding()
.background(Color.yellow)
.cornerRadius(10)//<=here
Spacer()
}
Spacer()
}
.padding(5.0)
}
We have a much better way to do this in iOS 14 using the ContainerRelativeShape.
ContainerRelativeShape is a new shape type that will take on the path of the nearest container shape specified by a parent with an appropriate corner radius based on the position of the shape.
In this instance, the system is defining the container shape for our widget, and we get the corner radius concentric with it super easy.
.background(ContainerRelativeShape().fill(Color.yellow))
var body: some View {
VStack {
HStack {
VStack {
Text("Hello")
.font(.title)
Text("Widget")
}
.padding()
.background(ContainerRelativeShape().fill(Color.yellow)) //<=here
Spacer()
}
Spacer()
}
.padding(5.0)
}

Update SwiftUI View size to match image

I'm trying to make a view which holds an image loaded asynchronously from a network request. Before the image loads, I want to show a placeholder view which has a fixed size. When the image loads, I want to replace this placeholder view with the image, scaled to fit inside the frame of the placeholder view, but then I want the parent view to shrink to match the size of this image. I can't figure out how to do this last part.
Right now, it looks like this:
struct ItemCell: View {
var body: some View {
Group {
CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
.aspectRatio(contentMode: .fit)
.frame(minWidth: 0, maxWidth: 150, minHeight: 0, maxHeight: 190, alignment: .bottomLeading)
}.background(Color.red) // To show that the view isn't resizing properly
}
}
struct PlaceholderView: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 3, style: .continuous)
.frame(width: 150, height: 190)
.foregroundColor(Color(.secondarySystemBackground))
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: 50)
.foregroundColor(.secondary)
}
}
}
The CustomImageView is adapted from this article on loading images asynchronously. The ItemCells are placed in a horizontal ScrollView. When I test this, it:
correctly displays the placeholder view before the image is loaded;
resizes the image so it maintains its aspect ratio and fits inside the 150x190 frame, but has a weird animation where some of the images shrink and then expand back; also, some of the images seem to shrink too much;
does not resize the parent view to match the size of the image properly, but instead retains the full original height and some (?) extra width on some cells.
These two problems are shown in the gif below, with blue images and a red background. Notice the extra height on the first and third cells, and the extra width on the second. Also, note that the first image ends up smaller than when it first loads, even though it fit inside the original 150x190 frame at first.
How can I fix these problems?
Figured out how to do it. There were several problems with my original code. First, the ItemCells used in the ScrollView should be modified with the .fixedSize() view modifier, like so:
ScrollView(...) {
HStack(...) {
ForEach(...) { ...
ItemCell()
.fixedSize()
}
}
}
Then, changing the frame of the CustomImageCell to be use idealHeight instead of maxHeight and making the Group a VStack with a Spacer() to push everything to the bottom, as #Paulw11 had suggested in comments:
struct ItemCell: View {
var body: some View {
VStack {
Spacer()
CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 150, idealHeight: 190)
}
}
}
These changes fix both the image resizing animation issue and the extra space issue.

Avoid button styling its content in SwiftUI

I'm creating an iOS app using Apple's SwiftUI framework. As I need to detect if the user taps on a specific area of the screen, I obviously use a button.
The problem is that the area contains an Image and a Text, and as the button automatically gives its content the blue color, the image is also colored, so instead of being an Image it's just a blue rounded rectangle.
It is said that an image is worth a thousand words, and as I'm not good at explaining, here you have a graphic demonstration of what happens:
Outside the button (without button styling)
Inside the button (with button styling)
This happens because the button is adding .foregroundColor(.blue) to the image.
How can I avoid/disable the button adding style to its components?
EDIT: This is my button code:
ContentView.swift:
Button(action: {/* other code */}) {
PackageManagerRow(packageManager: packageManagersData[0])
}
PackageManagerRow.swift:
struct PackageManagerRow : View {
var packageManager : PackageManager
var body: some View {
VStack {
HStack {
Image(packageManager.imageName)
.resizable()
.frame(width: 42.0, height: 42.0)
Text(verbatim: packageManager.name)
Spacer()
Image(systemName: "checkmark")
.foregroundColor(.blue)
.opacity(0)
}.padding(.bottom, 0)
Divider()
.padding(.top, -3)
}
}
}
I think this is from the rendering mode for the image you are using.
Where you have Image("Cydia logo") (or whatever).
You should be setting the rendering mode like...
Image("Cydia Logo").renderingMode(.original)
You can also add a PlainButtonStyle() to your button to avoid iOS style behaviors.
Something like that with your example :
Button(action: {/* other code */}) {
PackageManagerRow(packageManager: packageManagersData[0])
}.buttonStyle(PlainButtonStyle())
I hope it will help you!
Another option is to not use a Button wrapper, but instead use tapAction directly on the Image to trigger your action when the image is pressed
HStack {
Button(action: {
print("Tapped")
}, label: {
Image("Logo").renderingMode(.original) // Add Rendering Mode
})
Text("Cydia")
}
A button with an icon! How original 😀.
If you are dealing with SF symbols then the following will do fine:
Button(action: addItem) {
Text(Image(systemName: "plus").renderingMode(.original))
+
Text("This is Plus")
}
.font(.system(size: 42))
The limitation of the option above is you don't have control over Image's size. So for custom images the following is more appropriate:
Button(action: addItem) {
Label(
title: { Text("Label").font(.system(size: 40)) }, // Any font you like
icon: { Image(systemName: "rectangle.and.pencil.and.ellipsis") // Both custom and system images, i.e. `Image("Cydia logo")`
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 42, height: 42) // Any size you like
.padding() // Any padding you need
} // and etc.
)
}
Apply the style .plain to your button to avoid overlay color.
// Before
Button(...)
// After
Button(...)
.buttonStyle(.plain) // Remove the overlay color (blue) for images inside Button
.plain button style, that doesn’t style or decorate its content while idle, but may apply a visual effect to indicate the pressed, focused, or enabled state of the button.
Another solution is to custom the style with ButtonStyle
like: struct MyButtonStyle:ButtonStyle { }
You have to render the original image by adding .renderingMode(.original) right after your image declaration.
Image("your_image_name")
.renderingMode(.original)

Resources