Search code examples
iosswiftgoogle-cloud-firestoreasync-awaitfirebase-storage

swift - how to properly upload multiple images and await return values to then save in firebase db


I am currently working on an app and I have been stuck on a problem involving completion handlers and async/await.

I am trying to upload multiple files to fire storage and return the file name and url in a simple custom struct:

struct PicData: Codable, Equatable {
    let id: String
    let link: URL
}

I use this function to upload a single image. I changed the completion return from "Void" to my struct, but both give me problems in my next function.

private func uploadPicture(picture: UIImage, completion: @escaping (PicData) -> PicData) async throws {
    guard let data = picture.jpegData(compressionQuality: 1.0) else {
        throw StorageError(message: "No data found in picture")
    }
    let fileName = UUID().uuidString + ".jpg"
    let picRef = storage.child("photos").child(someId).child(fileName)
    
    picRef.putData(data, metadata: nil) { (metadata, error) in
        if let error = error {
            print("Error while uploading file: ", error)
            return
        }
        picRef.downloadURL(completion: { (url, error) in
            if let theUrl = url {
                completion(PicData(id:fileName, link:theUrl))
            }
        })            
    }
}

I use the following function to upload multiple images. It's the main upload function Im calling from my View model -

 func uploadPictures(_ pics: [UIImage],  completion: @escaping ([PicData])->()) async throws {
        try await withThrowingTaskGroup(of: PicData.self, body: { group in
            for (pic) in pics {
                group.addTask {
                    //let combo =
                    try await self.uploadPicture(picture: pic, completion: { (files)  in
                        return files
                    })
                    //return combo
                }
            }
            var fileNames: [PicData] = []
            for try await fileName in group{
                fileNames.append(PicData(id: fileName.id, link: fileName.link))
            }
            completion(fileNames)
        })
    }

And in my view model, I am trying to wait for the data before calling another function:

 func createNew(data: data, controller: UIViewController) async {
        self.isLoading = true
        Task{
            do {
               try await storageRepository.uploadPictures(data.pictures, completion: { pics in
                   Task {
                       do {
                           try await self.firestoreRepository.create(data: data, pictures: pics)
                           
                           DispatchQueue.main.async {
                               self.isLoading = false // when this is published next view is shown.
                           }
                       }
                   }
                })
                
            } catch {
                return
            }
        }
    }

I've spent a lot of hours trying to search and figure things out, and I understand about async/await and completion handlers. I've rewritten my functions multiple times in different ways and the images are indeed successfully uploaded but my other functions don't wait. I understand the thread isn't waiting for a response, but having a hard time fixing it.

One of the main errors I keep running into other than it not waiting, is when I am trying to get the value from a function -

// from function: uploadPictures
    Cannot convert value of type '()' to closure result type 'PicData'

(That issue though is probably better off as it's own question)

I've also tried using Semaphores although my understanding is very limited --

 actor Datas {
        var combos: [PicData] = []
        func append(combo: PicData) {
            combos.append(combo)
        }
    }
    
func runSema(pics: [UIImage]) async -> [PicData] {
    let combo = Datas()
    let queue = DispatchQueue(label: "com.app.myQueue", attributes: .concurrent)
    let semaphore = DispatchSemaphore(value: pics.count)
    for pic in pics {
        queue.async {
            semaphore.wait()
            Task {
                do {
                    let com = try await self.uploadPicture(picture: pic)
                    print(com)
                    await combo.append(combo: com)
                } catch {
                    
                }
            }                
            semaphore.signal()
        }
    }
    return await combo.combos
}

private func uploadPicture(picture: UIImage) async throws -> PicData {
    let semaphore = DispatchSemaphore(value: 1)
    guard let data = picture.jpegData(compressionQuality: 1.0) else {
        throw StorageError(message: "No data found in picture")
    }
    let fileName = UUID().uuidString + ".jpg"
    let picRef = storage.child("users").child(userId!).child(fileName)
    let metadata = StorageMetadata()
    metadata.contentType = "image/jpg"
    
    DispatchQueue.global().async {           
        semaphore.wait()
        picRef.putData(data, metadata: metadata) { (metadata, error) in
            if let error = error {
                print("Error while uploading file: ", error)
                //  return
            }
            picRef.downloadURL { (url, error) in
                guard let theUrls = url else { return }
                semaphore.signal()
            }
        }            
    }
    let theUrl = try await picRef.downloadURL() // trying to get url outside of closure        
    return(PicData(id: fileName, link: theUrl))
}

and in my view model -

 func createNew(data: data, controller: UIViewController) {
        self.isLoading = true
        Task{
            do {
                let combos = await storageRepository.runSema(pics: profileData.pictures)
                
                try await self.firestoreRepository.create(data: data, pictures: combos)
                
                DispatchQueue.main.async {
                    self.isLoading = false // when this is published next view is shown.
                }
                
            } catch {
                publishError(message: error.localizedDescription)
                return
            }
        }
    }

My files are uploaded but I can not get it to wait regardless what I've tried. I need to upload the files to storage then after, save my struct's data to Firestore DB.

I also tried to use putDataAsync with code I found on another SO answer, but kept getting this error -

Value of type 'StorageReference' has no member 'putDataAsync'

func someUpload(someImagesData: [Data]) async throws -> [String]  {
    // your path
    let storageRef = storage.child("users").child(userId!).child("fileName")
    
    return try await withThrowingTaskGroup(of: String.self) { group in
        
        // the urlStrings you are eventually returning
        var urlStrings = [String]()
        
        // just using index for testing purposes so each image doesnt rewrites itself
        // of course you can also use a hash or a custom id instead of the index
        
        // looping over each image data and the index
        for (index, data) in someImagesData.enumerated() {
            
            // adding the method of storing our image and retriving the urlString to the task group
            group.addTask(priority: .background) {
                let _ = try await storageRef.putDataAsync(data)
                return try await storageRef.downloadURL().absoluteString
            }
        }
        
        for try await uploadedPhotoString in group {
            // for each task in the group, add the newly uploaded urlSting to our array...
            urlStrings.append(uploadedPhotoString)
        }
        // ... and lastly returning it
        return urlStrings
    }
}

At this point I give up, any ideas where I'm going wrong on this?


Solution

  • Thanks to @loremipsum's comments, it lead me in the right direction to get it figured out.

    First I tried updating my packages via File -> Packages -> Update.. and clean building, reset package cache, resolve versions, close Xcode, rebuild, etc. and nothing worked. What I had to do was remove the Firebase iOS SDK and then add a fresh install.

    After doing so, a lot of errors popped up that originally wasn't there and didn't cause any problems before. Such as force unwrapping, and guard let statements, and I also had to add try await to some firebase calls. I was now able to use putDataAsync.

    Using the link provide by @loremipsum I was able to modify my code and get it to work -

      ///Uploads Data to the designated path in `FirebaseStorage`
        func upload(picture: UIImage) async throws -> PicData {
            let fileName = UUID().uuidString + ".jpg"
            let imageRef = storage.child("someRef")
            guard let data = picture.jpegData(compressionQuality: 1.0) else {
                throw StorageError(message: "No data found in picture")
            }
            let metadata = StorageMetadata()
            metadata.contentType = "image/jpg"
            let meta = try await imageRef.putDataAsync(data, metadata: metadata)
            let url = try await imageRef.downloadURL()
            return PicData(id: fileName, link: url)
        }
    
        ///Uploads a multiple images as JPEGs and returns the URLs for the images
        ///Runs the uploads simultaneously
        func uploadAsJPEG(pics: [UIImage]) async throws -> [PicData]{
            return try await withThrowingTaskGroup(of: PicData, body: { group in
                for pic in pics {
                    group.addTask {
                        return try await self.upload(picture: pic)
                    }
                }
                var urls: [PicData] = []
                for try await url in group{
                    urls.append(url)
                }
                
                return urls
            })
        }
        
        
        func uploadAsJPEG1x1(images: [UIImage]) async throws -> [PicData]{
            var urls: [PicData] = []
            for image in images {
                let url = try await upload(picture: image)
                urls.append(url)
            }
            return urls
        }
        
    

    And then in my view model I was able to call using try await and it actually waited this time.