SwiftUI: Change Popover Arrow Color - ios

In SwiftUI, how does one change the color of the arrow that connects a popover to its anchor point?
When working with the underlying UIPopoverController outside of SwiftUI, I believe it's done by changing the backgroundColor property, but I don't see a way to access that here. Even setting background as the very last modifier only changes the view within the popover; not the popover itself.
For example, adding the following code to a view:
#State private var showDetailedView: Bool = false
// ...
.popover(isPresented: self.$showDetailedView) {
Text("Hello!")
.padding()
.background(Color.red)
}
.onTapGesture {
self.showDetailedView = true
}
...results in an arrow that's still the default background color (this example taken from native macOS in Dark Mode):
...and like this on iOS (running via Catalyst), which is even worse!

Here is a pure SwiftUI solution using GeometryReader and two .frame calls. The key idea is to make a background larger than the size of your presented view. Since SwiftUI does not clip contents at this moment, this will override the default background on the popover arrow.
Do notice that this only works with a solid background. In Catalyst, a solid background is already painted so transparent content would reveal the ugly black as you have posted. We might have to resort to things like UIViewRepresentable for such case.
Consider the following example that changes the color of an arrow on the top edge:
.background(GeometryReader { geometry in
Color
.white
.frame(width: geometry.size.width,
height: geometry.size.height + 100)
.frame(width: geometry.size.width,
height: geometry.size.height,
alignment: .bottom)
})
Explanation:
The first inner frame creates a white rectangle that is 100px higher than your presented view.
The second outer frame creates a new frame that is of the same size as your presented view. this is achieved through the GeometryReader.
The alignment: argument in the second outer frame makes sure that these two frames align on the bottom.
Since the outer frame is as large as the GeometryReader, it fills the whole background of your presented view.
The "overflowed" content overrides the default black arrow color.
To make this work with arbitrary arrow edge, you might want to change the inner frame, increasing both the width and height. As for the alignment for outer frame, using the default argument of .center should work.

Related

How to ignore parent view's padding SwiftUI

I have a VStack with multiple child views (the one with blue background). The VStack has horizontal padding. I want to have this padding set for each child, but sometimes I have exception where I want that child to reach edges of the display completely (Two grey lines above "Checkout" button). Is there any way how to allow this to happen? I don't wanna set padding for every single child separately.
You can apply a negative padding on the view that you applied on the VStack, that means if you applied a padding of 16 points to the VStack like this for example .padding(16) for all directions which is the default. then you can apply a .padding(.horizontal,-16) to the lines and they will stretch to the end of the screen
here is a sample code and a screenshot for the behavior you want.
struct VStackPadding: View {
var body: some View {
VStack{
RoundedRectangle(cornerRadius: 4)
.frame(width: .infinity,height: 3)
.padding(.horizontal, -16)
.padding(.bottom,16)
RoundedRectangle(cornerRadius: 4)
.frame(width: .infinity,height: 3)
}.padding(16)
}
}

Can't display images with shadows and scroll smoothly

My app involves displaying <100 image thumbnails and for some reason my iPad Pro 2018 is struggling to scroll through the images smoothly. I recreated a simplified example below. The image is 200px square.
Replacing the images with colored rectangles eliminates the lag. Removing the shadow also removes the lag. I think rendering 50 images with a shadow should be within my device's capabilities, but let me know if anyone disagrees.
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
let gridLayout = [(GridItem(.adaptive(minimum: 160)))]
LazyVGrid(columns: gridLayout, spacing: 8) {
ForEach(0..<50) { index in
Image("cookie_200")
.resizable()
.aspectRatio(1.0, contentMode: .fit)
.padding(8)
.shadow(radius: 4)
}
}
})
}
}
Question
Is there a less performance-intensive way to show these thumbnails?
Screen capture with shadow (laggy scroll):
https://share.icloud.com/photos/04eFNISH1khfkFqgAmGfGJJZw
Screen capture without shadow (smooth):
https://share.icloud.com/photos/02etl7kVG30Cnc6cr_dwSJK-Q
Image:
Shadows and transparency make the runtime do a lot of work. Hence the lag.
Question Is there a less performance-intensive way to show these thumbnails?
Yes. Instead of making the runtime draw the shadows, you draw the shadows. In particular you make an image consisting of the thumbnail and the shadow, on an opaque background the same color as your view background. Now scrolling is perfectly performant.

Image at top of view extending under navigationbar

I am trying to make a "reusable" template for views in my app. As part of this I started prototyping this:
var body: some View {
NavigationView {
ZStack {
VStack {
// Spacer()
Image("progress_bar")
.resizable()
.scaledToFit()
.foregroundColor(Color.gray)
.background(Color.green)
HStack{
}
Spacer()
}
VStack{
}
}
}
.navigationBarHidden(true)
}
}
The ZStack contains 2 VStack. First one is my template and will be part of multiple of my screens later on. Second Stack is destined to be replaced by #ViewBuilder parameter so that I can reuse that in multiple screens easily.
The progress_bar image is a SVG file imported into assets, preserving Vector Data and rendered as template (So I can change colour).
My issue, as shown on the following screenshot, is that the image somehow extends toward the top of the screen. The green area correspond to the green coloured background added to the image. The progress bar is the grey line across the screen.
progress bar extending toward top of the screen
If I change my code to (commented out the spacer):
// Spacer()
Image("progress_bar")
.resizable()
.scaledToFit()
.foregroundColor(Color.gray)
.background(Color.green)
HStack{
}
Spacer()
}
I get this, progress bar shifts down in the screen (not wanted but expected) but the green area that was added on top of the image disappears:
updated screen with progress_bar shifted down and not over extending
I did try setting up a maxHeight to my Image view but this did not work out.
What am I missing? Is there a way I can stop this from happening?
Edit:
After more looking around, my issue is coming from the fact that the whole thing is embedded in a NavigationView. Apparently space is saved for the navigation bar even though it is hidden.

Context menu preview not with rounded corners in SwiftUI

When using a plain styled List in SwiftUI with more than one Text view inside a VStack as show below, the preview of the view when showing its context menu doesn't have corner radius. If you remove one of the Text views it will have corner radius. Also the rows that you need to scroll down to will also show corner radius most of the time. I've tried using the contentShape modifier with RoundedRectangle but doesn't fix it. How can I get it to show with corner radius all the time?
List {
ForEach(1...20, id: \.self) { _ in
VStack {
Text("Hello")
Text("World")
}.contextMenu {
Button {} label: { Text("Hello") }
}
}
}.listStyle(.plain)
I thinks you should fill a bug to Apple in this case.
After some try I have notice that if you define a frame for your Label , the view is correctly rounded.
I tryed to fixedSize the text without success :
Text("world").fixedSize()
The problem seems to come from _UIMorphingPlatterView, more precisely the _UIPlatterClippingView which use _UIPortalView.
If the frame is not fixed the clip is not apply correctly.
The debug view hierarchy give this :

How to stop GeometryReader changing value when keyboard opens

I'm struggling to find documentation to support this but it seems as though the values of
GeometryReader.size.width & height change when the keyboard opens. This can be proven through something like:
var body: some View {
TabView {
GeometryReader { g in
Rectangle()
.frame(width:g.size.width/2,height:g.size.height/20)
TextField(...)
}
}
}
which shows the rectangle resizing when the keyboard opens by clicking on the textfield.
How would I prevent this from happening? I want to specify the frame size relative to screen size to support many screen sizes...
You don't need a geometry reader to know the screen's size. you can get screen's dimensions using UIScreen.main.bounds.width and UIScreen.main.bounds.height.
Note width always shows the horizontal-dimension's size, and height always shows the vertical one (incase of screen rotation)
Add this to the GeometryReader:
.ignoresSafeArea(.keyboard)

Resources