Home > OS >  How to use downloaded URL correctly in AsyncImage?
How to use downloaded URL correctly in AsyncImage?

Time:10-19

How to use downloaded URL from getData class in AsyncImage?

struct RecentItemsView: View {
    var item: dataType             // var's from getData class
    var body: some View {
        HStack(spacing: 15) {
            AsyncImage(url: URL(string: item.pic), content: { image in   // item.pic here
                image.resizable()
                }, placeholder: {
                    ProgressView()
                    })

I have full URL from downloadURL but when Im using item.pic parameter in AsyncImage I get error: (See Image)

I understand that the error contains the path to the image, which is not suitable for AsyncImage, that's why I downloaded the full URL, the question is how to use the received URL in AsyncImage?

class getData : ObservableObject {
    
    @Published var datas = [dataType]()
    
    init() {
        
        let db = Firestore.firestore()
        
        db.collection("items").getDocuments { (snap, err) in
            
            if err != nil {
                
                print((err?.localizedDescription)!)
                return
            }
            
            for i in snap!.documents {
                
                let id = i.documentID
                let title = i.get("title") as! String
                let description = i.get("description") as! String
                let pic = i.get("pic") as! String
                
                self.datas.append(dataType(id: id, title: title, description: description, pic: pic))
                
                let storage = Storage.storage()
                let storageRef = storage.reference().child("\(pic)")
                
                storageRef.downloadURL { url, error in
                    if let error = error {
                        print("Failed to download url:", error)
                        return
                    } else {
                        
                        print(url!)             // Full Url- https://firebasestorage.googleapis.com:...
                        
                    }
                }
            }
        }
    }
}

struct dataType : Identifiable {
     
    var id = UUID().uuidString
    var title : String
    var description : String
    var pic : String
}

Error:

Error

Storage:

enter image description here

Firestore:

enter image description here

CodePudding user response:

This is going to look quite a bit different from your current approach but give it a try, it will simplify your code overall.

Main differences are the use of async await and FirebaseFirestoreSwift.

I choose using async await/Concurrency because it provides a more linear approach to the code and I think resolves your issue about sharing the variable with all the objects.

This is what your ObservableObject will look like

//Keeps UI Updates on the main thread
@MainActor
//Classes and structs should always be uppercased
class GetData : ObservableObject {
    
    @Published var datas = [DataType]()
    private var task: Task<Void, Never>? = nil
    init() {
        task = Task{
            do{
                try await getData()
            }catch{
                //Ideally you should present this error
                //to the users so they know that something has gone wrong
                print(error)
            }
        }
    }
    deinit{
        task?.cancel()
    }
    func getData() async throws {
        let documentPath = "items"
        let svc = FirebaseService()
        //async await allows a more linear approach. You can get the images individually
        var items : [DataType] = try await svc.retrieve(path: documentPath)
        
        for (idx, item) in items.enumerated() {
            //Check if your url is a full url
            if !item.pic.localizedCaseInsensitiveContains("https"){
                //If it isnt a full url get it from storage and replace the url
                items[idx].pic = try await svc.getImageURL(imagePath: item.pic).absoluteString
                //Optional update the object so you dont have to retrieve the
                //The url each time.
                try svc.update(path: documentPath, object: items[idx])
            }
        }
        datas = items
    }
}

and your struct should change to use @DocumentID.

//This is a much simpler solution to decoding
struct DataType : Identifiable, FirestoreProtocol {
    @DocumentID var id : String?
    //If you get decoding errors make these variables optional by adding a ?
    var title : String
    var description : String
    var pic : String
}

Your Views can now be modified to use the updated variables.

@available(iOS 15.0, *)
public struct DataTypeListView: View{
    @StateObject var vm: GetData = .init()
    public init(){}
    public var body: some View{
        List(vm.datas){ data in
            DataTypeView(data: data)
        }
    }
}
@available(iOS 15.0, *)
struct DataTypeView: View{
    let data: DataType
    var body: some View{
        HStack{
            Text(data.title)
            AsyncImage(url: URL(string: data.pic), content: { phase in
                switch phase{
                case .success(let image):
                    image
                        .resizable()
                        .scaledToFit()
                        .frame(width: 50, height: 50)
                case .failure(let error):
                    Image(systemName: "rectangle.fill")
                        .onAppear(){
                            print(error)
                        }
                case .empty:
                    Image(systemName: "rectangle.fill")
                @unknown default:
                    Image(systemName: "rectangle.fill")
                    
                }
            })
        }
        
    }
    
}

The class GetData is pretty bare bones an uses the code below to actually make the calls, I like using generics to simplify code and so it can be reused by various places.

You don't have to completely understand what is going on with this now but you should, I've put a ton of comments so it should be easy.

import FirebaseStorage
import FirebaseFirestore
import FirebaseFirestoreSwift
import SwiftUI
import FirebaseAuth
struct FirebaseService{
    private let storage: Storage = .storage()
    private let db: Firestore = .firestore()
    ///Retrieves the storage URL for an image path
    func getImageURL(imagePath: String?) async throws -> URL{
        guard let imagePath = imagePath else {
            throw AppError.unknown("Invalid Image Path")
        }
        typealias PostContinuation = CheckedContinuation<URL, Error>
        //Converts an completion handler approach to async await/concurrency
        return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
            storage.reference().child(imagePath).downloadURL { url, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else if let url = url {
                    continuation.resume(returning: url)
                } else {
                    continuation.resume(throwing: AppError.unknown("Error getting image url"))
                }
            }
        }
    }
    ///Retireves the documetns from the Firestore and returns an array of objects
    func retrieve<FC>(path: String) async throws -> [FC] where FC : FirestoreProtocol{
        let snapshot = try await db.collection(path).getDocuments()
        return snapshot.documents.compactMap { doc in
            do{
                return try doc.data(as: FC.self)
            }catch{
                //If you get any decoding errors adjust your struct, you will
                //likely need optionals
                print(error)
                return nil
            }
        }
    }
    ///Updates the provided document into the provided path
    public func update<FC : FirestoreProtocol>(path: String, object: FC) throws{
        guard let id = object.id else{
            throw AppError.needValidId
        }
        try db.collection(path).document(id).setData(from: object)
    }
}

enum AppError: LocalizedError{
    case unknown(String)
    case needValidId
}

protocol FirestoreProtocol: Identifiable, Codable{
    ///Use @DocumentID from FirestoreSwift
    var id: String? {get set}
}

All of this code works, if you put all this code in a .swift file it will compile and it should work with your database.

  • Related