2

I'm fetching data from Firestore, mapping the documents and decoding each of them using FirestoreDecoder. However, decoding the documents momentarily freezes the UI. Running the code on the background thread makes no difference. How can I prevent the UI from freezing during the decoding?

    let collection = Firestore.firestore().collection("roll_groups")
    
    collection.addSnapshotListener { (snapshot, error) in
        if let error = error {
            print("Error fetching roll groups: \(error.localizedDescription)")
        } else if let snapshot = snapshot {
            DispatchQueue.global(qos: .background).async {
                let rollGroups = snapshot.documents.map { doc -> RollGroup? in
                    do {
                        let rollGroup = try FirestoreDecoder().decode(RollGroup.self, from: doc.data())
                        return rollGroup
                    } catch {
                        print("Error decoding roll groups: \(error)")
                        return nil
                    }
                }
                
                DispatchQueue.main.async {
                    completion(rollGroups)
                }
            }
        }
    }
James Anderson
  • 556
  • 3
  • 16
  • 41
  • Looks fine to me. I'd use the Time Profiler in Instruments to pinpoint the bottleneck if you can. It's likely something that's happening in the completion handler itself. – Bradley Mackey Jun 17 '21 at 16:19
  • @BradleyMackey Thanks, will do! Weirdly if I remove the completion handler entirely it still freezes, so I don't think it's anything to do with that :( – James Anderson Jun 17 '21 at 17:00
  • If you remove the completion handler then there isn't any decoding going on but the UI still freezes? – trndjc Jun 18 '21 at 19:11
  • You don't need the DispatchQueue.main.async, and I don't see any UI calls in your code. Within Firebase closures, networking is all done on a background thread, and UI calls are done on the main thread automatically so it will never interfere with the UI. Since you're using a third party library, the issue may lie there and be unrelated to Firebase. – Jay Jun 19 '21 at 13:27
  • @liquid Sorry for the confusion, just meant I removed `completion(rollGroups)`, not the whole snapshot listener completion handler :) – James Anderson Jun 21 '21 at 12:46
  • @Jay Agreed, it seems to be an issue with the way `CodableFirebase` is decoding it. I'd be happy to explore alternatives, like `JSONDecoder`, though I need to be able to parse `FIRTimestamp` and it seems there is no straightforward way to do this? – James Anderson Jun 21 '21 at 12:49
  • @Jay why doesn't he need `DispatchQueue.main.async`? The snapshot return enters into the background to parse the documents and then hops back onto the main to call completion. If this function is called by an object that updates its UI in this completion handler, which it almost certainly does based on his question, then he should undoubtedly use `DispatchQueue.main.async` in this closure. – trndjc Jun 21 '21 at 14:51
  • 1
    I'd ditch this library because IMO these third-party dependencies should be used for much bigger things, like road maps and database interfacing. And if the only reason you're using it is to abstract the encoding/decoding of Firestore's timestamp object then I wouldn't even think twice about getting rid of it. You can convert the Firestore timestamp into something that you can use with `Codable` like a unix-timestamp (which is a `UInt64` in Swift), an ISO-8601 date (https://www.hackingwithswift.com/example-code/language/how-to-use-iso-8601-dates-with-jsondecoder-and-codable), among others. – trndjc Jun 21 '21 at 15:01
  • @liquid Firebase closures perform UI calls on the main thread so DispatchQueue is typically unnecessary, and as I mentioned, I didn't see any UI calls in the code. Enclosing the completion in a DispatchQueue didn't appear to help either. See [this](https://stackoverflow.com/questions/65036886/do-i-need-background-thread-and-loader-for-retrieving-data-from-firebase/65039976#65039976) and [this](https://stackoverflow.com/questions/39178165/firebase-asynchronous-function-whats-in-the-background-queue-and-whats-not). And I upvoted your suggestion to ditch the library if it's causing contention. – Jay Jun 21 '21 at 17:11
  • @Jay what I'm saying is that the OP dispatched the document parsing to a background thread and then hopped back onto main to call the container function's completion handler which is how it should be done. The reason we don't see any UI updates is because they happen in the object calling this function, hence the completion handler and hence the need to call it on the main. – trndjc Jun 21 '21 at 20:18
  • @JamesAnderson Can you please provide a minimal example repo link where it can be reproduced? – Tarun Tyagi Jun 24 '21 at 10:25

1 Answers1

1

Possible Solution

Looking at this code it all seems fine, I just want to confirm that the completion for your method is definitely a @escaping: completion() otherwise it could cause this issue

also, it might be worth wrapping the actual DB call (collection.addSnapshotListener) in

DispatchQueue.global(qos: .background).async

Just to see if that Makes a difference, in theory it shouldn't but nonetheless it's worth a shot

Ayrton CB
  • 454
  • 2
  • 9