I'm trying to display a long list of images with titles using the new AsyncImage in SwiftUI. I noticed that when I put VStack around AsyncImage and scroll through images it's reloading images every time I scroll up or down. When there's no VStack I see no reloading and images seem to stay cached.
Is there a way to make VStack not to reload images on scrolling so I can add text under each image?
Here's a working example. Try scrolling with and without VStack.
import SwiftUI
struct TestView: View {
let url = URL(string: "https://picsum.photos/200/300")
let columns: [GridItem] = [.init(.fixed(110)),.init(.fixed(110)),.init(.fixed(110))]
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(0..<20) { _ in
// VStack here causes to images to reload on scrolling
VStack {
AsyncImage(url: url) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "photo")
.imageScale(.large)
.frame(width: 110, height: 110)
}
}
}
}
}
}
}
I fully agree with you that this is a weird bug. I would guess that the reason it's happening has something to do with the way LazyVGrid chooses to layout views, and that using a VStack here gives it the impression there is more than one view to show. This is a poor job on Apple's part, but this is how I solved it: just put the VStacks internal to the AsyncImage. I'm not entirely sure what the original error is, but I do know that this fixes it.
struct MyTestView: View {
let url = URL(string: "https://picsum.photos/200/300")
let columns: [GridItem] = [.init(.fixed(110)),.init(.fixed(110)),.init(.fixed(110))]
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(0..<20) { i in
AsyncImage(url: url) { image in
VStack {
image
.resizable()
.aspectRatio(contentMode: .fit)
Text("label \(i)")
}
} placeholder: {
VStack {
Image(systemName: "photo")
.imageScale(.large)
.frame(width: 110, height: 110)
Text("image broken")
}
}
}
}
}
}
}
Related
I want to have an alert style view that can include an icon image at the top along with the title.
alert(isPresented:content:) used with Alert does not seem to support adding images in any way. However, other than that limitation, it functions as I want my alert to function. It covers the status bar and blurs the main view as the background for the alert.
I attempted to use fullScreenCover(isPresented:onDismiss:content:) instead, but this approach does not behave like an alert as far as covering up EVERYTHING including the status bar, and blurring the main view as the background.
I have tried this so far, with the following result. One of the reasons I want it to behave like a normal Alert is so that the content can scroll without overlapping the clock time.
struct AlertView: View {
#EnvironmentObject var dataProvider: DataProvider
var alert: Watch.AlertMessage
var body: some View {
ScrollView {
Image("IconAlert")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.foregroundColor(.accentColor)
Text("Test Title")
.bold()
Text("Test Description")
Button("DISMISS") { print("dismiss") }
}
.padding()
.ignoresSafeArea()
.navigationBarHidden(true)
}
}
Try this this sample custom alert. It may give you some great approach(Code is below the image):
struct CustomAlert: View {
#State var isClicked = false
var body: some View {
ZStack {
Color.orange.edgesIgnoringSafeArea(.all)
Button("Show Alert") {
withAnimation {
isClicked.toggle()
}
}
ZStack {
if isClicked {
RoundedRectangle(cornerRadius: 20)
.fill(.thickMaterial)
.frame(width: 250, height: 250)
.transition(.scale)
VStack {
Image("Swift")
.resizable()
.frame(width: 150, height: 150)
.mask(RoundedRectangle(cornerRadius: 15))
Button("Dismiss") {
withAnimation {
isClicked.toggle()
}
}
.foregroundColor(.red)
}
.transition(.scale)
}
}
}
}
}
My SwiftUI app displays images from an external url properly using
LazyVGrid(columns: columns, alignment: .center, spacing: 10) {
ForEach(0..<14) { i in
AsyncImage(url: url) { image in
VStack {
image
.resizable()
.scaledToFill()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.onTapGesture {
selectedItem = ImageSelection(name: url!.path)
print(selectedItem?.name as Any)
}
}
}
.sheet(item: $selectedItem) { item in
Image(item.name)
}
But the sheet that comes up from the .onTapGesture is blank. How can I properly get the url path so the image displays on the new sheet? Thanks!
EDIT
Ultimately this view is displaying images from https://picsum.photos. I'm trying to determine the actual URL of the displayed images.
as #Asperi mentioned, you could use another AsyncImage to again download the image. Try the following code, which
fixes some of the inconsistencies in your code and also loops over
the url id (as per your latest thought) to download each different ones:
struct ImageSelection: Identifiable {
let id = UUID()
var url: URL? // <-- note
}
struct ContentView: View {
let columns:[GridItem] = Array(repeating: .init(.flexible(), spacing: 5), count: 3)
#State var selectedItem: ImageSelection?
var body: some View {
ScrollView {
LazyVGrid(columns: columns, alignment: .center, spacing: 10) {
ForEach(0..<14) { i in
let url = URL(string: "https://picsum.photos/id/\(i)/200")
AsyncImage(url: url) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.onTapGesture {
selectedItem = ImageSelection(url: url)
}
}
else {
Image(systemName: "smiley")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
}
}
}
}
.sheet(item: $selectedItem) { item in
AsyncImage(url: item.url)
}
}
}
}
I'm trying to get a basic thing, but I can't make it work!
I would like to have a VStack that contains a text an image and a second text.
Everything should be visible on the screen. So the Image should resize (crop top and bottom) to give spaces for the two texts... but not.
Without .scaledToFill() it's working well, but the image is stretched
The problem (I think) is because the image has a high height!
(I tried with GeometryReader, fixedSize, layoutPriority, but nothing that I tried works)
The image is from Wikipedia: Image link
struct TestView: View {
var body: some View {
VStack(spacing: 8) {
Text("Text 1")
Image("image")
.resizable()
.scaledToFill()
.padding()
Text("Text 2")
}.background(Color.blue)
}
}
Simulator screen
What is needed:
Image of what is needed
Thank you :)
Ok, I found a way
Color.clear
.background(Image("image")
.resizable()
.scaledToFill())
.clipped()
Embedding the image as background of a clear Color, the image is limited to the parent size!
Result: https://imgur.com/a/ceZRqSw
struct TestView: View {
var body: some View {
VStack(spacing: 8) {
Text("Text 1")
Image("image")
.resizable()
.scaledToFill()
.padding()
Text("Text 2")
}
.background(Color.blue)
.padding(.top)
}
}
You could also use the maxHeight frame parameter to frame the view if you know how big you'd want it to be. You can also do this on the image itself.
struct TestView: View {
var body: some View {
VStack(spacing: 8) {
Text("Text 1")
Image("image")
.resizable()
.scaledToFill()
.padding()
Text("Text 2")
}
.background(Color.blue)
.frame(maxHeight: 500) //500 can be any value of your choice
}
}
You can also use the AsyncImage with placeholder to load the image
struct ContentView: View {
var body: some View {
VStack(spacing: 8) {
Text("Text 1")
AsyncImage(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/Isabella_of_France_by_Froissart.png/1024px-Isabella_of_France_by_Froissart.png")){ image in
image.resizable()
}placeholder: {
ProgressView()
}
.scaledToFill()
.frame(width: 200, height: 400, alignment: .center)
Text("Text 2")
}
.background(Color.blue)
}
}
It seems like you are looking for a way to consider Safe Area. because at the second image all contents respect to safe area while the first image don't. However the code seems to do so by default unless we indicate to ignore safe area by using .ignoresSafeArea(). Look at the view where you called TextView, You might use ZStack with a blue background that affects its children.
If not you should used scaledToFit instead of scaledToFill because as I see both images, the second one is cropped.
Have you tried
struct TestView: View {
var body: some View {
VStack(spacing: 8) { // You can alternatively increase the spacing and remove the padding for the image
Text("Text 1")
Image("image")
.resizable()
.scaledToFill()
.padding()
Text("Text 2")
}.background(Color.blue)
.padding() // Note padding here
}
}
I have a Content View which looks like this:
The code for the Content View is:
var body: some View {
ZStack(alignment: .bottom) {
VStack {
if selectedTab == "gearshape" {
GearshapeView()
}
}
CustomTabBar(selectedTab: $selectedTab)
}
}
and this is the code of the GearshapeView:
var body: some View {
Color.blue
.edgesIgnoringSafeArea(.all)
}
Now I want to add a picture in the background and keep the size of the menu same like before.
var body: some View {
Image("s7")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea()
}
Unfortunately when I resize the picture to fit the screen, the menu also resizes.
Any ideas on how to keep it static and not stretched?
Solved it by using Geometry reader:
GeometryReader { geo in
Image("s7")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width, height: geo.size.height)
}
.edgesIgnoringSafeArea(.all)
When there is a NavigationLink in a container with an Image (that is resizable, scaled to fill, and clipped to a smaller frame), the NavigationLink cannot be pressed. I'm assuming that this has to do with the parts of the Image that have been "clipped off" still actually present and blocking the NavigationLink.
Here is a short example to replicate the behavior:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
VStack {
NavigationLink(destination: Text("Hello, world!")) {
Text("Press me")
}
}
Image("background")
.resizable()
.scaledToFill()
.frame(height: 60)
.clipped()
}
}
}
}
"background" can be any sort of picture from the assets folder.
I have tried to mess around with the zIndexes; that didn't work.
There was one hack that worked: I used a UIImage, cropping it to the aspect ratio of Image I wanted by converting it to a CGImage and back into a UIImage. After doing that, I could press on the NavigationLink again but it was obvious from my phone lagging that it was too expensive. I tried to work around this by saving the cropped image to the documents directory and then whenever the aspect ratio wasn't similar enough I would recrop, save, and reload the image, but this still took a toll on the performance of my project.
Please offer some advice on how I should handle this situation. Thanks in advance for any help.
Here is alternate to zIndex (if other active elements are present in view as well) - disable user interaction with background image
Image("background")
.resizable()
.scaledToFill()
.frame(height: 60)
.clipped()
.allowsHitTesting(false) // << here !!
//.zIndex(-1) // << also force put below siblings
Set .zIndex(1.0) to VStack of NavigationLink.
Tested : XCode 12.2, iOS 14.1
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
VStack {
NavigationLink(destination: Text("Hello, world!")) {
Text("Press me")
}
}
.zIndex(1.0) //<--- here
Image("ivana-cajina-_7LbC5J-jw4-unsplash")
.resizable()
.scaledToFill()
.frame(height: 60)
.clipped()
}
}
}
}
Here is another alternative.
public var body: some View {
ZStack {
image
.resizable()
.scaledToFill()
// > etc..
.allowsHitTesting(false)
}
.clipped()
}