0

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

        }
    }

1 Answers1

2

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)
}
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
  • Thanks @Sh_Khan. This solve my immediate issue and is appreciated! One additional question here, if I add another async function (let's say I want to init the corresponding Author object from a returned DocumentReference -> Snapshot block, do I increment the Dispatch Group with another enter/leave call? I assume yes and want to confirm that I should call enter right BEFORE executing the async call, and leave RIGHT after all work is completed. – dmwatson0101 Dec 16 '19 at 04:29