How to get AsyncImage url path from url with SwiftUI - swift

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

Related

Swift - recognize a link and make it clickable

I have an app that loads blog posts from a Wordpress site.
Now loading the text, images and video's work fine.
In the blog posts from Wordpress are links. Like for example: https://eenvandaag.avrotros.nl/item/ondanks-droogte-op-de-veluwe-onttrekt-kartonfabriek-parenco-er-jaarlijks-miljarden-liters-grondwater/
When the app loads the post, the link is displayed in plain text.
Is there a way to recognise the links and make these "links" clickable and guide the user to safari?
I've been searching the internet for hours but couldn't find a fitting solution.
Every post has a different link. So the goal is that the link is recognised and made clickable.
I found some examples, for example:
var body: some View {
VStack {
Text(attributedString)
Text("Check out [this link](https://stack.com)")
}
}
var attributedString: AttributedString {
var attributedText = AttributedString("Visit website")
attributedText.link = URL(string: "https://stack.com")
return attributedText
}
But then you have to set the link.
So that doesn't work for me since the link is different for every post.
Can someone point me in the right direction?
This is the code for my PostContentView
import AVKit
import CachedAsyncImage
import SwiftUI
import WPArticleView
struct PostContentView: View {
let post: Post
var body: some View {
VStack(alignment: .leading, spacing: 32) {
if let url = post.embedded.medias?.first?.url {
CachedAsyncImage(url: url, urlCache: .imageCache) { phase in
switch phase {
case.empty:
ProgressView()
.progressViewStyle(.circular)
.frame(maxWidth: .infinity)
case.success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.cornerRadius(8)
.frame(height: 200)
.padding(.bottom, 16)
.padding(.top, -10)
case.failure:
ZStack{
Image(systemName: "wifi.slash")
.foregroundColor(Color(UIColor.white))
RoundedRectangle(cornerRadius: 6)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 200)
.frame(maxWidth: .infinity)
}
#unknown default:
EmptyView()
}
}
}
VStack(alignment: .leading, spacing: 16) {
Text(post.title.text)
.font(.title2).bold()
.padding(.horizontal)
.foregroundColor(.white)
Text(post.date.formattedDateString)
.font(.footnote)
.padding(.horizontal)
.foregroundColor(.white)
WPArticleView(htmlBody: post.content.rendered) { blocks in
ForEach(blocks.indices, id: \.self) { id in
blocks[id]
.padding(.bottom, 6)
.padding(.horizontal)
.foregroundColor(.white)
.textSelection(.enabled)
}
} text: { attributedText in
Text(attributedText)
} image: { imageURL in
CachedAsyncImage(url: imageURL, urlCache: .imageCache) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.cornerRadius(8)
} placeholder: {
//EmptyView()
ProgressView()
.frame(maxWidth: .infinity)
}
} video: { videoURL in
VideoPlayer(player: AVPlayer(url: videoURL))
.frame(minHeight: 250)
//.frame(minHeight: 400)
.cornerRadius(8)
}
}
}
.padding(.horizontal, 20)
}
}
And my Post screen:
import Foundation
struct Post: Decodable, Identifiable {
let id: Int
let date: Date
let title: Title
let content: Content
let embedded: Embedded
enum CodingKeys: String, CodingKey {
case date = "date"
case title = "title"
case content = "content"
case embedded = "_embedded"
case id = "id"
}
}
EDIT:
Here is an example from a post:

Swiftui failed to produce diagnostic for expression; please submit a bug report

Before I select an image from the gallery, I want to have an initial image from SF-Symbole ("photo.fill). But unfortunately it shows me an error.
Failed to produce diagnostic for expression; please submit a bug
report (https://swift.org/contributing/#reporting-bugs) and include
the project
The code:
import SwiftUI
struct ContentView: View {
#State private var showImagePicker : Bool = false
#State private var image : Image? = nil
var body: some View {
NavigationView{
VStack {
Spacer()
if image? {
Image(uiImage: image?)
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity)
} else {
Image(systemName: "photo.fill")
.resizable()
.scaledToFit()
.opacity(0.6)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.horizontal)
}
Spacer()
Button("Öffne Galerie"){
self.showImagePicker = true
}.padding()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)
}
.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
.navigationBarTitle(Text("Foto bearbeiten"))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As I mentioned in my comment you are unwrapping the optional image incorrectly, you need to use an if-let so that you have a non-nil value for your image.
You are also passing an Image where you actually require a UIImage. This is not something that you can do. You need to make sure that you understand that types that you using and what they represent.
Unfortunately you did not include the code for your PhotoCaptureView so it is not possible to see what you actually require. This is why you are asked to provide a minimum reproducible example when you post a question on SO.
However, here is how you can handle it if it requires an Image or a UIImage, there are similarities to both. Look at the comments in the code for the changes.
PhotoCaptureView uses Image
If you are creating an Image in the PhotoCaptureView then there are two changes that you need to make.
You need to unwrap your Image before you use it
As you have a SwiftUI Image you can just use it directly.
struct ContentView: View {
#State private var showImagePicker : Bool = false
#State private var image : Image? = nil
var body: some View {
NavigationView{
VStack {
Spacer()
if let image = image { // here we unwrap the Image
image // as we have a SwiftUI Image we can use it directly.
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity)
} else {
Image(systemName: "photo.fill")
.resizable()
.scaledToFit()
.opacity(0.6)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.horizontal)
}
Spacer()
Button("Öffne Galerie"){
self.showImagePicker = true
}.padding()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)
}
.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
.navigationBarTitle(Text("Foto bearbeiten"))
}
}
}
PhotoCaptureView uses UIImage
However, if your PhotoCaptureView requires a UIImage then you need to make three changes to your code.
Firstly we would need to change your #State variable from being Image into a UIImage.
We can then unwrap the image the same way as above
We then pass the unwrapped UIImage into the initializer for Image(uiImage:)
struct ContentView: View {
#State private var showImagePicker : Bool = false
#State private var image : UIImage? = nil // Here we use UIImage
var body: some View {
NavigationView{
VStack {
Spacer()
if let image = image { // We unwrap the UIImage so that we can use it
Image(uiImage: image) // Here we convert the UIImage into a SwiftUI Image
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity)
} else {
Image(systemName: "photo.fill")
.resizable()
.scaledToFit()
.opacity(0.6)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.horizontal)
}
Spacer()
Button("Öffne Galerie"){
self.showImagePicker = true
}.padding()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)
}
.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
.navigationBarTitle(Text("Foto bearbeiten"))
}
}
}
I would suggest that you read up on Optionals in the Swift documentation, as you seem to have some misconceptions about them.

SDWebImageSwiftUI - How to show default image before user selection

I have a profile picture that I am attempting to show a default image until a user selects a picture from a picker (say that three times fast). In WebImage, I point to a view Model, which connects to my database to grab the user image. If no image exists I currently just have it on an empty string.
For reference, this is the line that serves up the image
WebImage(url: URL(string: vm.userModel?.profilePictureURL ?? "")
I tried adding a bool that updated on the onChange function to true and tried wrapping the webImage in an IF statement, but the view didn't update correctly.
import SwiftUI
import Firebase
import FirebaseFirestore
import SDWebImageSwiftUI
struct ProfilePicture: View {
#ObservedObject var vm = DashboardLogic()
#State private var showingImagePicker = false
#State private var inputImage: UIImage?
var body: some View {
ZStack (alignment: .topTrailing){
VStack{
if let inputImage = inputImage {
Image(uiImage: inputImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:150, height: 150)
.clipShape(Circle())
} else{
WebImage(url: URL(string: vm.userModel?.profilePictureURL ?? ""))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:150, height: 150)
.clipShape(Circle())
}
Button(action: {
showingImagePicker = true
}){
Image(systemName:("plus.circle.fill"))
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundColor(Color("ButtonTwo"))
.frame(width:30, height:25)
.contentShape(Rectangle())
}
//PRESENT PICKER
}
.sheet(isPresented: $showingImagePicker){
EditorImagePicker(image: $inputImage)
}
}
//SAVE IMAGE TO DATABASE (FIREBASE)
.onChange(of: inputImage, perform: { _ in
persistImageToStorage() //call to save function
})
}
}
}

Using TextField hides the ScrollView beneath it in VStack

This view hold a list of pdf names which when tapped open webviews of pdf links.
The view has a search bar above the list which when tapped causes the scrollview to disappear.
struct AllPdfListView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#ObservedObject var pdfsFetcher = PDFsFetcher()
#State var searchString = ""
#State var backButtonHidden: Bool = false
#State private var width: CGFloat?
var body: some View {
GeometryReader { geo in
VStack(alignment: .leading, spacing: 1) {
HStack(alignment: .center) {
Image(systemName: "chevron.left")
Text("All PDFs")
.font(.largeTitle)
Spacer()
}
.padding(.leading)
.frame(width: geo.size.width, height: geo.size.height / 10, alignment: .leading)
.background(Color(uiColor: UIColor.systemGray4))
.onTapGesture {
self.mode.wrappedValue.dismiss()
}
HStack(alignment: .center) {
Image(systemName: "magnifyingglass")
.padding([.leading, .top, .bottom])
TextField ("Search All Documents", text: $searchString)
.textFieldStyle(PlainTextFieldStyle())
.autocapitalization(.none)
Image(systemName: "slider.horizontal.3")
.padding(.trailing)
}
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.black, lineWidth: 1))
.padding([.leading, .top, .bottom])
.frame(width: geo.size.width / 1.05 )
ScrollView {
ForEach($searchString.wrappedValue == "" ? pdfsFetcher.pdfs :
pdfsFetcher.pdfs.filter({ pdf in
pdf.internalName.contains($searchString.wrappedValue.lowercased())
})
, id: \._id) { pdf in
if let parsedString = pdf.file?.split(separator: "-") {
let request = URLRequest(url: URL(string: "https://mylink/\(parsedString[1]).pdf")!)
NavigationLink(destination: WebView(request: request)
.navigationBarBackButtonHidden(backButtonHidden)
.navigationBarHidden(backButtonHidden)
.onTapGesture(perform: {
backButtonHidden.toggle()
})) {
HStack(alignment: .center) {
Image(systemName: "doc")
.padding()
.frame(width: width, alignment: .leading)
.lineLimit(1)
.alignmentGuide(.leading, computeValue: { dimension in
self.width = max(self.width ?? 0, dimension.width)
return dimension[.leading]
})
Text(pdf.internalName)
.padding()
.multilineTextAlignment(.leading)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
.padding(.leading)
}
}
}
.navigationBarHidden(true)
}
.accentColor(Color.black)
.onAppear{
pdfsFetcher.pdfs == [] ? pdfsFetcher.fetchPDFs() : nil
}
}
}
}
}
Pdf list and Searchbar.
The same view on Searchbar focus.
I would like the search string to filter the list of pdfs while maintaining the visibility of the list.
I was able to fix this by making my #ObservableObject an #EnvironmentObject in my App :
#main
struct MyApp: App {
#ObservedObject var pdfsFetcher = PDFsFetcher()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(pdfsFetcher)
}
}
}
struct AllPdfListView: View {
#EnvironmentObject var pdfsFetcher: PDFsFetcher
}

Using VStack with AsyncImage inside LazyVStack causes images to reload on scrolling

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