How to call data from firebase synchronously - arrays

I have a function that calls data from my Firebase Database, but it is an async function, and messing up the other functions of my app. I have looked all around tutorials but they only show asyncronus functions, and my app depends on the Firebase Data to load first.
Code for the function:
#State var realLat: [String] = [ ]
#State var realLong: [String] = [ ]
func downloadFirebaseData() async throws -> Float {
let group = DispatchGroup()
group.enter() // << start
let db = Firestore.firestore()
try withUnsafeThrowingContinuation { continuation in
db.collection("annotations")
.getDocuments { (querySnapshot, error) in
defer {
group.leave() // << end on any return
}
// result heading code here
if let Lat = i.document.get("lat") as? String {
DispatchQueue.main.async {
realLat.append(Lat)
print("downloadLatServerData() \(realLat)")
}
}
}
}
group.wait() // << block till leave
}
The function DownloadFirebaseMapServerData() is an async function becuase of the line db.collection("annotations").addSnapshotListener {(snap, err) in... and I need realLat and realLong to be downloaded first inorder to assign them to a mapAnnotation, so is there any way that I could make this function syncronus or make another function with the same end goal? Also another thing to note is that realLat and realLong are both String Arrays, or Arrays that are Strings

Ok, let's say that - try to avoid making synchronous API that designed asynchronous - it was done by purpose, long time-consuming bla-bla-bla
but, if it is needed technically it is possible, for example using dispatch groups, like
func downloadFirebaseData() async throws -> Float {
let group = DispatchGroup()
group.enter() // << start
let db = Firestore.firestore()
try withUnsafeThrowingContinuation { continuation in
db.collection("annotations")
.getDocuments { (querySnapshot, error) in
defer {
group.leave() // << end on any return
}
// result heading code here
}
}
group.wait() // << block till leave
}

Related

Why does this function append the object twice to the array each time it runs?

I'm using swift's DispatchGroup() to help orchestrate a for loop that
finds a document in firesbase
converts the document to a custom object
appends the custom object to an array
With each pass, the function ends up appending each object twice to the array and I can't understand why.
Here is the function...
func getFriends() {
// Initialize the DispatchGroup
let group = DispatchGroup()
// the myFriends array contains documentIDs that I am using to fetch documents from firebase
//
for pid in myFriendObj.myFriends {
group.enter()
_ = Firestore.firestore().collection("Players")
.whereField(FieldPath.documentID(), isEqualTo: pid)
.addSnapshotListener { [self] querySnapshot, error in
if let error = error {
print("Error getting > Players: \(error.localizedDescription)")
return
}
guard let querySnapshot = querySnapshot else { return }
self.players.append(
contentsOf: querySnapshot.documents.compactMap { document in
try? document.data(as: UserProfile.self)
})
group.leave()
}
}
group.notify(queue: DispatchQueue.global(qos: .background)) {
// I'm currently eliminating the dups via this fancy extends method.
self.players = self.players.removeDuplicates()
}
}
:: UPDATE ::
Still no luck on this - i've even removed dispatchgroup and the snapshotlistener callbacks and still this code calls get() twice when an instance of the class is instantiated. Here is the new, more simple code...
class FriendRepository: ObservableObject {
private let store = Firestore.firestore()
private let friendPath: String = "MyFriends"
#Published var friendIDs: [String] = []
var userId = ""
private let authenticationService = AuthenticationService()
private var cancellables: Set<AnyCancellable> = []
init() {
authenticationService.$user
.compactMap { user in
user?.uid
}
.assign(to: \.userId, on: self)
.store(in: &cancellables)
authenticationService.$user
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.get()
}
.store(in: &cancellables)
}
func get( ) {
store.collection(friendPath).document(userId).getDocument {(document, error) in
let result = Result {
try document?.data(as: Friends.self)
}
switch result {
case .success(let f):
if let f = f {
print("friends:>> \(f.myFriends)")
self.friendIDs = f.myFriends
} else {
print("Document does not exist")
}
case .failure(let error):
print("Error decoding city: \(error)")
}
}
}
When a new instance run init(), I see this in the console... It prints the friends:>> statement twice
friends:>> ["PHyUe6mAc3LodM5guJJU"]
friends:>> ["PHyUe6mAc3LodM5guJJU"]
Each time a change happens in the database, your addSnapshotListener closure gets called with all data that matches the query - even if that data wasn't change since the last call. This typically means that you'll want to empty self.players at the top of the callback, or loop over the documentChanges collection to determine exactly what changed.
func getFriends() {
// this will empty the players array when ever the get friends function gets called.
self.players.removeAll()
// Initialize the DispatchGroup
let group = DispatchGroup()
// the myFriends array contains documentIDs that I am using to fetch documents from firebase
//
for pid in myFriendObj.myFriends {
group.enter()
_ = Firestore.firestore().collection("Players")
.whereField(FieldPath.documentID(), isEqualTo: pid)
.addSnapshotListener { [self] querySnapshot, error in
if let error = error {
print("Error getting > Players: \(error.localizedDescription)")
return
}
guard let querySnapshot = querySnapshot else { return }
self.players.append(
contentsOf: querySnapshot.documents.compactMap { document in
try? document.data(as: UserProfile.self)
})
group.leave()
}
}
}

Wait till image is done loading to an array before appending the array to another array

I have an array with data I get from Firebase like this:
profileImage = UIImage(data: data!)!
array.append(CustomModel(creatorID:creatorID, creatorPhoto: profileImage, creatorName: creatorName, documentID: documentID))
Now I need to append that array into another array like this
self.arrayOfArrays.append(array)
However when I append array it seems to leave it as empty, as (im guessing) the image is still loading onto array.
My Question Is: Can I use some sort of Dispatch or delayed that would wait till array is done loading before appending it to arrayOfArrays?
PS: I tried using DispatchQueue.main.asyncAfter(deadline: .now() + 5) and it works, but I need something that is not time based.
Also I cannot have the line just under the code:
array.append(CustomModel(creatorID:creatorID, creatorPhoto: profileImage, creatorName: creatorName, documentID: documentID))
because this will ruin the structure of the code.
why don't you use reactive approach using RxSwift & RxCocoa. You can achieve that this way:
Let assume that this is your struct as Codable:
struct CustomModel: Codable {
let creatorID: String
let creatorPhoto: Data
let creatorName: String
let documentID: String
}
Next create your firebase client that will create observable for fetching CustomModel from firebase database
class FirebaseClient {
static var shared = FirebaseClient()
lazy var firebaseRequestObservable = FirebaseRequestObservable()
func getCustomModel() throws -> Observable<CustomModel> {
return requestObservable.getCustomModel()
}
}
Next you must implement your observable with getCustomModel method that will return your CustomModel from firebase. I have set child name as CustomModel, but you can set it depending on your firebase structure. Also here you could return an array of data like [CustomModel].Also we add onNext, onError and onCompleted methods that will return data or error or complete our subscription to observable.
public class FirebaseRequestObservable {
let citiesRef = db.collection("CustomModels")
public init() {
}
//MARK: function for URLSession takes
public func getCustomModel<CustomModel: Decodable>() -> Observable<CustomModel> {
//MARK: creating our observable
return Observable.create { observer in
Database.database().reference().child("CustomModel").observeSingleEvent(of: .value, with: { (snapshot) in
guard let value = snapshot.value else { return }
do {
let customModel = try FirebaseDecoder().decode(CustomModel.self, from: value)
observer.onNext(customModel)
} catch let error {
observer.onError(error)
}
//MARK: observer onCompleted event
observer.onCompleted()
})
return Disposables.create {
task.cancel()
}
}
}
}
And finally in your ViewController call client getCustomModel method from client class that will return your data asynchronously.
class ViewController: UIViewController {
var customModel: CustomModel
var array: [CustomModel] = []
var arrayOfArrays: [[CustomModel]] = []
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let client = FirebaseClient.shared
do{
try client.getCustomModel().subscribe(
onNext: { result in
//result is custom model from firebase
self.customModel = result
//append your data
self.array.append(self.customModel)
self.arrayOfArrays.append(array)
},
onError: { error in
print(error.localizedDescription)
},
onCompleted: {
print("Completed event.")
}).disposed(by: disposeBag)
}
catch{
}
}
}

Modifying array from asynchronous call not modifying the array swift

I'm trying to modify an array of objects that I retrieve from an asynchronous call with the results from another asynchronous call. Basically I retrieve an array of results and there is a field messages that is returned nil from my server call. I then need to make another server call with the id of each result in a for loop to get the messages array. I then set the messages to the returned value.
I just learned about using DispatchGroup() about 20 minutes ago from https://stackoverflow.com/a/48718976/3272438, so bear with me.
The issue I get is that when I do print("C0: (self.res)") it prints out all of the items including the messages I just added. When print("C2: (self.res)") prints in the group.notify(...), all of the messages fields print out nil. I'm stumped and I have tried everything I could think of. Any help would be much appreciated.
UPDATE
By changing the following group.notify() never gets called
Service.getMessages(resultId: self.res![index].id, completionHandler: { (latestMessage, response, error) in
if let latestMessage = latestMessage {
self.res![index].messages = [latestMessage]
print("L2: \(self.res![index].messages)")
print("C0: \(self.res)")
}
group.leave() // continue the loop
})
ViewController.Swift
var res: [ResultDTO]?
override func viewDidLoad() {
super.viewDidLoad()
let group0 = DispatchGroup()
group0.enter()
Service.getResults { (results, response, error) in
guard var results = results else { print("PROBLEM"); return }
self.res = results
group0.leave()
}
group0.notify(queue: .main) {
print("in group0")
let group = DispatchGroup() // initialize
for index in 0..<self.res!.count {
group.enter() // wait
Service.getMessages(resultId: self.res![index].id, completionHandler: { (latestMessage, response, error) in
if let latestMessage = latestMessage {
self.res![index].messages = [latestMessage]
print("L2: \(self.res![index].messages)")
print("C0: \(self.res)")
}
})
group.leave() // continue the loop
}
group.notify(queue: .main) {
print("In group notify")
do {
print("C2: \(self.res)")
for index in 0..< self.res!.count {
print("L4: \(self.res![index].messages)")
}
// ... configure my view with the data
} catch {
print("Error: \(error)")
}
}
}
}
I would simplify your data handling by using one dispatch group (since one is all you need):
class YourViewController: UIViewController {
private var results: [ResultDTO]?
override func viewDidLoad() {
super.viewDidLoad()
getResults()
}
private func getResults() {
Service.getResults { (results, response, error) in
guard let results = results,
results.count > 0 else {
return
}
let dispatchGroup = DispatchGroup()
for r in results {
dispatchGroup.enter()
Service.getMessages(resultId: r.id, completionHandler: { (latestMessage, response, error) in
if let latestMessage = latestMessage {
r.messages = [latestMessage]
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main, execute: {
self.results = results
})
}
}
}
Consider this only a starting point. If this were production code, I'd implement error handling and background queueing (assuming the data is returned on the main thread).

Load Firestore data to Table view Swift 4

I have a question related to load data from Firestore to table view. Basically, i understand how to do it, but in this case, i kind of confuse if the structure of Firestore as below:
"fund" -> "randomId" -> "agensi: id"
i was able to load the agensi from collection which is "WJ18kBvDLhVrvml2slXB". To get the real agensi name i have to get data from collection "Agensi" as image below:
below is the code that what i have already did:
var agencyname: [String] = []
func getData() {
db.collection("fund").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
// var agencyNumber = 0
for document in querySnapshot!.documents {
let data = document.data()
let agency = data["agensi"] as? String ?? ""
let agencyId = document.documentID
print(agency)
print(agencyId)
//MARK get name
let newdocRef = Firestore.firestore().document("Agensi/\(agency)")
newdocRef.getDocument { (docSnapshot, error) in
guard let docSnapshot = docSnapshot, docSnapshot.exists else { return }
let dataDetails = docSnapshot.data()
let agencyNew = dataDetails!["name"] as? String ?? ""
self.agencyname.append(agencyNew)
print("List of the agency: \(self.agencyname.append(agencyNew))")
}
}
self.tableView.reloadData()
}
}
}
i try to print:
self.agencyname.append(agencyNew)
but its display nothing. so, i cannot load the name of the agency into my table view cell. By the way sorry for my bad english. Thanks in advance
There are few things I would like to tell:
1. FireStore queries are asynchronous so there is a callback function when it finishes. Reload your tableView inside the callback instead of end of the loop only then it will load the data properly.
DispatchQueue.main.async {
self.tableView.reloadData()
}
Write above code just below print("List of the agency: \(self.agencyname.append(agencyNew))") line.
2. You're printing "self.agencyname.append(agencyNew)" and it is void by the way so will not print anything so print(\(self.agencyname)) instead.
3. When using a guard in a loop then don't use return because it will break the loop for next iteration if any error occurs. We should use continue here to let the loop execute completely.

How to properly add data from Firebase into Array?

I'm trying to get results from Firebase and put them into Array, but it seems I miss something. What I want is to get 'Time' and 'Blood Glucose" values from Firebase and to put them into an arrays which I will use for Charts. I'm able to put the data into 'BG' and 'TIME' arrays, but when I 'append' them into 'FetchedDate' and 'FetchedBG' I see empty arrays (FetchedBG and FetchedDate)
var FetchedDate:[String]! = []
var FetchedBG: [Double]! = []
//GET DATA FROM FB
func GetDetails(){
let posts = rootRef.child("Diary/\(userID!)/\(passedDATE!)")
//let posts = rootRef.queryOrderedByChild(passedDATE!)
posts.observeEventType(FIRDataEventType.Value , withBlock: { (snapshot) in
for list in snapshot.children {
if let BG = list.value.objectForKey("Blood Glucose")!.doubleValue {
self.FetchedBG.append(BG)
print(BG) // SHOWS RESULTS AS EXPECTED
}
if let TIME = list.value.objectForKey("Time") {
self.FetchedDate.append(TIME as! String)
print(TIME) // SHOWS RESULTS AS EXPECTED
}
}
}) { (error) in
print(error.localizedDescription)
}
}
override func viewDidLoad() {
super.viewDidLoad()
GetDetails()
print(FetchedDate) // EMPTY ARRAY
print(FetchedBG) // EMPTY ARRAY
Firebase loads (and synchronizes) the data from your database asynchronously. Since that may take some time, your code continues executing and you print the arrays while they're still empty.
Once a value is available (either for the first time or when the data has changed), your block is invoked. It adds the data to the arrays. But by that time your print statements have long finished.
The solution is to move the code that needs to run when the value is available (or when it has changed) into the block. E.g.
var FetchedDate:[String]! = []
var FetchedBG: [Double]! = []
//GET DATA FROM FB
func StartSynchronizingDetails(){
let posts = rootRef.child("Diary/\(userID!)/\(passedDATE!)")
//let posts = rootRef.queryOrderedByChild(passedDATE!)
posts.observeEventType(FIRDataEventType.Value , withBlock: { (snapshot) in
for list in snapshot.children {
if let BG = list.value.objectForKey("Blood Glucose")!.doubleValue {
self.FetchedBG.append(BG)
print(BG) // SHOWS RESULTS AS EXPECTED
}
if let TIME = list.value.objectForKey("Time") {
self.FetchedDate.append(TIME as! String)
print(TIME) // SHOWS RESULTS AS EXPECTED
}
}
print(FetchedDate)
print(FetchedBG)
}) { (error) in
print(error.localizedDescription)
}
}
override func viewDidLoad() {
super.viewDidLoad()
StartSynchronizingDetails()
This is a very common pattern when your app interacts with (potentially time-consuming) network resources. It is also precisely the reason Firebase's observeEventType takes a withBlock: argument: to isolate the code that starts synchronizing from the code that responds to value updates.

Resources