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.
Related
I'm trying to add an SVG image to my SwiftUI application and the edges around the image are jagged. How can I fix this?
I have verified that the actual SVG image is not jagged. I also tried adding .interpolation(.none) with no success. As well as enabling Preserve Vector Data, which didn't work either.
It almost looks like it's not treating the image as an SVG, because changing the foreground color property doesn't have any impact.
HStack {
if let satelliteImage = UIImage(named: "satellite-dish-solid") {
Image(uiImage: satelliteImage)
.resizable()
.frame(width: 45, height: 45)
.foregroundColor(.primary)
}
Text("Satellite")
}
can anyone tell how to make Responsive UI in Swift UI, which is compatible on all Device , and using figma Text like 24 then it shows iphone 8 and iphone 11 same view
You'll want to minimize the amount of hardcoded sizes or frames in your app as that will make your app less responsive across different phone models and orientations. Here are some links you can check out to learn responsive UI:
https://www.youtube.com/watch?v=67ZCQ5ihj_I&t=186s
https://www.youtube.com/watch?v=ALzrixd_hd8
Consider trying out geometry reader as well. Here's an article that goes into depth on what it is and how to use it: https://www.hackingwithswift.com/quick-start/swiftui/how-to-provide-relative-sizes-using-geometryreader
For images you can do something like this. You can use GeometryReader class to make your images responsive. Do not use hardcoded size for images. In this snippet I used 50% width of the screen and for height it will also take 50% of the height.
Also Be careful using GeometryReader class. The GeometryProxy returns the current view width and height.
var body: some View {
GeometryReader { reader in
VStack {
Image(contact.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: reader.size.width * 0.5, height: reader.size.width * 0.5)
.clipped()
.cornerRadius(reader.size.width)
.shadow(radius: 10)
}
}
}
A better way to do it without wrapping all your views in a geometry reader is to make an extension to get the screen with and height, which is also callable on all pages without having to do the extension on every page like a geometryreader.
extension View{
func getScreenBounds() -> CGRect{
return UIScreen.main.bounds
}
}
and to call it is the same as geometry reader:
.frame(width: getScreenBounds().width * 0.5, height: getScreenBounds().width * 0.5
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.
Consider the following example:
struct ContentView: View {
#State var showSplash: Bool = true
#Namespace var animationNamespace
var body: some View {
ZStack {
if showSplash {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
.frame(width: geometry.size.width)
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.all)
.clipped()
} placeholder: {
Color.gray
}
}
.onTapGesture {
toggleSplashScreen(false)
}
} else {
ScrollView {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image
image
.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
} placeholder: {
Color.gray
}
.frame(width: geometry.size.width, height: 400)
.clipped()
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
toggleSplashScreen(true)
}
}
}
}
}
}
With a helper method here:
private extension ContentView {
func toggleSplashScreen(_ toggle: Bool) {
withAnimation(.spring(response: 0.85, dampingFraction: 0.95)) {
showSplash = toggle
}
}
}
This produces:
I noticed two things here that I would like to fix
The flashing white effect when transitioning between the two states.
I noticed since we are using AsyncImage, when showSplash changes the AsyncImages only sometimes hits the placeholder block. As a result, the transition becomes really choppy. I tested this with a static image from the assets file and the transition then became smooth. I also tried creating a Caching mechanism on the AsyncImage but still had issues with it hitting placeholder block sometimes.
Would love to hear any ideas :) Thanks!
There are a couple of things that I think you could do to improve this.
First, You are fighting a little bit against the way SwiftUI maintains a view's identity. One of the ways that SwiftUI determines when it can reuse an existing structure as opposed to recreating a structure, is by it's location in the view hierarchy. So when you toggle your structure you go from:
GeometryReader
AsyncImage
to
ScrollView
GeometryReader
AsyncImage
As a result, the system thinks these are two AsyncImage views and so it's rebuilding the view (and reloading the image) every time. I think that's where your white flashes come from since you're seeing your gray placeholder in the middle of your animation. If you could leave the scroll view in place, possibly disabling scrolling when it's not needed (if that's possible) then the OS could maintain the identity of the AsyncImage. (see https://developer.apple.com/videos/play/wwdc2021/10022/)
That leads to the second area of investigation for you. AsyncImage is wonderful in the convenience it gives you in loading content from the network. Unfortunately it doesn't make that communication faster. Your goal should be to have AsyncImage go to the network as few times as possible.
Right now, your resizing strategy focuses on resizing the image. That means that for every transition you're "hitting the network" (read putting your code on the slow, dusty, dirt road path). Instead of resizing the image, you should just load the image once (the slow part) and resize the view that is displaying it. The general idea would be to let AsyncImage load the image, then control how the image is animated by animating the frame of the view.
This is where I get less helpful. I don't know enough about AsyncImage to know if it's capable of implementing that strategy. It seems that it should be... but I don't know that it is. You might have to resort to downloading and storing the image as state separately from the view that presents it.
So my advice is to limit the number of times AsyncImage has to reload the network data. That involves helping SwiftUI maintain the identity of the AsyncImage so it doesn't have to reload each time the view is created. And, try to implement your animations and scaling on the view, not the image, because rescaling the image also requires a network reload.
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.