0

I have an iOS widget in which I would like to update the Widget View after I retrieve data from Firestore. I have looked all over the internet, including at this question. I am unable to update the view after the data is retrieved. Please look at the following code for the widget's view. The data is retrieved, but I am unable to update the UI element after I retrieve the data as I would like.

struct Runner_WidgetEntryView: View {
    @State var text = ""
    @State var textLoaded = false
    var entry: Provider.Entry
    
    var body: some View {
        ZStack {
            Image("back").resizable().scaledToFit()
            Rectangle().foregroundColor(.black).cornerRadius(10).padding([.leading, .trailing], 10).padding([.top, .bottom], 15).overlay(Text(textLoaded ? text : "Loading..."))
        }.onAppear {
            let dispatchGroup = DispatchGroup()
            FirebaseApp.configure()
            let db = Firestore.firestore()
            dispatchGroup.enter()
            db.collection("collection").getDocuments { (snapshot, error) in
                if error != nil {
                    text = "There was an error retrieving the text."
                    dispatchGroup.leave()
                }
                if let snapshot = snapshot {
                    for document in snapshot.documents {
                        if document.documentID == "Document ID" {
                            text = document.data()["qoute_name"] as! String
                            dispatchGroup.leave()
                        }
                    }
                }
                else {
                    text = "There was an error retrieving the text."
                    dispatchGroup.leave()
                }
            }
            dispatchGroup.notify(queue: .main) {
                textLoaded = true
            }
        }
    }
}

How do I update the TextView after I retrieve the data?

Priyashree Bhadra
  • 3,182
  • 6
  • 23
Todd
  • 500
  • 4
  • 10

2 Answers2

0

SwiftUI needs to have its variables updated on the main thread. So if you are not seeing the update, it could be due to the different threads involved. Try this to update text on the main thread.

EDIT-1:

.onAppear {
    var fireTxt = ""  // <-- here
    let dispatchGroup = DispatchGroup()
    FirebaseApp.configure()
    let db = Firestore.firestore()
    dispatchGroup.enter()
    
    db.collection("collection").getDocuments { (snapshot, error) in
            if error != nil {
                fireTxt = "There was an error retrieving the text."  // <-- here
            }
            if let snapshot = snapshot {
                fireTxt = "No document found"  // <-- here default text
                for document in snapshot.documents {
                    if document.documentID == "Document ID" {
                        fireTxt = document.data()["qoute_name"] as! String   // <-- here
                    }
                }
            }
            else {
                fireTxt = "There was an error retrieving the text."  // <-- here
            }
            dispatchGroup.leave()
        }
    
    dispatchGroup.notify(queue: .main) {
        textLoaded = true
        text = fireTxt    // <-- here on the main thread
    }
}

Note, it is not a good idea to do all this db fetching in the .onAppear. You should use a view model for that.

  • Thank you for your answer. I tried it, but the TextView never changes from "Loading...". How do I make it so that either the error message or the text displays? – Todd Dec 31 '21 at 19:20
  • humm, works in my tests. Make sure the `Rectangle().foregroundColor(...)` is a color you can see, something like: `.foregroundColor(.green)` as a test. – workingdog support Ukraine Dec 31 '21 at 22:44
  • I can see 'Loading...' so it's not an issue with the foreground color. The state isn't updating, so it won't change to the retrieved text. – Todd Dec 31 '21 at 23:17
  • I see that there is some paths that are not catered for in your logic. Try adding `if snapshot.documents.isEmpty { fireTxt = "No documents in db"; dispatchGroup.leave() }` just before the `for document in snapshot.documents {...}` loop. Also if there is no document in `if document.documentID == "Document ID" {...}` there will be no text message and will not have `dispatchGroup.leave()` – workingdog support Ukraine Dec 31 '21 at 23:40
  • I've updated my answer with a new `if let snapshot = snapshot {...}` – workingdog support Ukraine Dec 31 '21 at 23:47
  • Thank you for the suggestions, however, it still doesn't work. Like I said before, I am getting the data from Firestore, I don't have any issue with that, however, I can update the TextView to read the text that I retrieved. I am able to get text from the Firestore every time. It just doesn't update in the view. I can still update the view within .onAppear, but just not within the `dispatchGroup.notify` or within the Firestore closure. – Todd Dec 31 '21 at 23:50
  • can you put a print in the `dispatchGroup.notify` using my code. – workingdog support Ukraine Jan 01 '22 at 00:15
  • I put one in `dispatchGroup.notify` right after `textLoaded = true; text = fireTxt` and it executed. I see the output of the print statement twice. – Todd Jan 01 '22 at 00:36
  • I'm not sure if this is relevant, but all the statements execute inside `dispatchGroup.notify`, but the variables don't change. I set a breakpoint, and the debugger shows that after I set `text = fireText`, `text` remains the same. – Todd Jan 01 '22 at 07:39
0

After building off of @workingdog's answer and a lot of testing, I have come up with this solution:

The SwiftUI state cannot be updated from a closure, so it cannot be updated from within dispatchGroup.notify or any other closure.

The workaround was to retrieve the data from Firebase when creating a timeline, and then saving the retrieved data to UserDefaults. From there, the data can be read from UserDefaults in the onAppear method in the SwiftUI view.

Todd
  • 500
  • 4
  • 10