Search code examples
iosswiftfirebase-realtime-databasefirebase-storagegrand-central-dispatch

Closure returning data before async work is done


UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION

I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.

If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is and/or refactoring for more efficiency / readability!

Proposed Solution with GCD and updated example

This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order

public func SetupArticleListener(completion: @escaping ([Article]) -> Void) {
        var returnArray = [Article]()
        let db = FIRdb.articles.reference()

        let listener = db.addSnapshotListener() { (querySnapshot, error) in
            returnArray = [] // nil this out every time
            if let error = error {
                print("Error in setting up snapshot listener - \(error)")
                } else {

                let fireStoreDispatchGrp = DispatchGroup() /// 1

                querySnapshot?.documents.forEach {
                    var dataDict = $0.data() //mutable copy of the dictionary data
                    let id = $0.documentID

//NEW EXAMPLE WITH ADDITIONAL TASK HERE
                    if let author = $0.data()["author"] as? DocumentReference {
                        author.getDocument() {(authorSnapshot, error) in
                            fireStoreDispatchGrp.enter() //1
                            if let error = error {
                                print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
                                fireStoreDispatchGrp.leave()
                                return
                            }

                            if let newAuthor = authorSnapshot.flatMap(Author.init) {
                                print("Able to build new author \(newAuthor)")
                                dataDict["author"] = newAuthor
                                dataDict["authorId"] = authorSnapshot?.documentID
                                print("Data Dict successfully mutated \(dataDict)")
                            }
                            fireStoreDispatchGrp.leave() //2
                        }

                    }

///END OF NEW EXAMPLE

                    if let imageURL = $0.data()["image"] as? String {
                        let reference = FIRStorage.articles.referenceForFile(filename: imageURL)

                        fireStoreDispatchGrp.enter() /// 2

                        self.getURL(reference: reference){ result in
                            switch result {
                            case .success(let url) :
                                dataDict["image"] = url.absoluteString

                            case .failure(let error):
                                print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
                            }

                            if let newArticle = Article(id: id, dictionary: dataDict) {
                                returnArray.append(newArticle)
                            }

                            fireStoreDispatchGrp.leave() ///3
                        }
                    }
                }
                //Completion block
                print("Exiting dispatchGroup all data should be setup correctly")
                fireStoreDispatchGrp.notify(queue: .main) { ///4
                completion(returnArray)

                }
            }
        }
        updateListeners(for: listener)
    }

Original Code

Calling Setup Code

self.manager.SetupArticleListener() { [weak self] articles in
                    print("🌈🌈🌈🌈🌈🌈🌈In closure function to update articles🌈🌈🌈🌈🌈🌈🌈")
                    self?.articles = articles

                }

Article Listener

 public func SetupArticleListener(completion: @escaping ([Article]) -> Void) {
        var returnArray = [Article]()
        let db = FIRdb.articles.reference()

        let listener = db.addSnapshotListener() { (querySnapshot, error) in
            returnArray = [] // nil this out every time
            if let error = error {
                printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
            } else {

                querySnapshot?.documents.forEach {
                    var dataDict = $0.data() //mutable copy of the dictionary data
                    let id = $0.documentID
                    if let imageURL = $0.data()["image"] as? String {
                        let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
                        self.getURL(reference: reference){ result in
                            switch result {
                            case .success(let url) :
                                print("Success in getting url from reference \(url)")
                                dataDict["image"] = url.absoluteString
                                print("Dictionary XFORM")

                            case .failure(let error):
                                print("Error retrieving URL from reference \(error)")
                            }

                            if let newArticle = Article(id: id, dictionary: dataDict) {
                                printLog("Success in creating Article with xformed url")
                                returnArray.append(newArticle)
                            }
                        }
                    }
                }
                print("🌈🌈🌈🌈🌈🌈🌈 sending back completion array \(returnArray)🌈🌈🌈🌈🌈🌈🌈")
                completion(returnArray)
            }
        }
        updateListeners(for: listener)
    }

GetURL

private func getURL(reference: StorageReference, _ result: @escaping (Result<URL, Error>) -> Void) {
        reference.downloadURL() { (url, error) in
            if let url = url {
                result(.success(url))
            } else {
                if let error = error {
                    print("error")
                    result(.failure(error))
                }
            }

        }
    }

Solution

  • You need dispatch group as the for loop contains multiple asynchronous calls

    public func SetupArticleListener(completion: @escaping ([Article]) -> Void) {
        var returnArray = [Article]()
        let db = FIRdb.articles.reference()
    
        let listener = db.addSnapshotListener() { (querySnapshot, error) in
            returnArray = [] // nil this out every time
            if let error = error {
                printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
            } else {
    
                let g = DispatchGroup() /// 1
                querySnapshot?.documents.forEach {
                    var dataDict = $0.data() //mutable copy of the dictionary data
                    let id = $0.documentID
                    if let imageURL = $0.data()["image"] as? String {
                        let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
                        g.enter() /// 2
                        self.getURL(reference: reference){ result in
                            switch result {
                            case .success(let url) :
                                print("Success in getting url from reference \(url)")
                                dataDict["image"] = url.absoluteString
                                print("Dictionary XFORM")
    
                            case .failure(let error):
                                print("Error retrieving URL from reference \(error)")
                            }
    
                            if let newArticle = Article(id: id, dictionary: dataDict) {
                                printLog("Success in creating Article with xformed url")
                                returnArray.append(newArticle)
                            }
    
                            g.leave() /// 3
                        }
                    }
                }
                g.notify(queue:.main) {   /// 4
                  print("🌈🌈🌈🌈🌈🌈🌈 sending back completion array \(returnArray)🌈🌈🌈🌈🌈🌈🌈")
                  completion(returnArray)
                }    
            }
        }
        updateListeners(for: listener)
    }