Swift Async call not updating data right away - swift

I am struggling to get the code below to work in my xcode project.
The first half of the code doesn't actually populate my code until around the cellforrowat function (not in the snippet below). Because of this the second half of the code snipit doesnt even run (because the movie array is still empty).
So I think my problem is that DispatchQueue.main.async {self.tableView.reloadData()} does not update my tableview right away.
var movies: [MovieDT] = []
var db: Firestore
var uid: String = ""
init(genre:Int, categoryNum:[Int], uid:String){
db = Firestore.firestore()
super.init(nibName: nil, bundle: nil)
self.uid = uid
TMDBConfig.apikey = "xxx"
GenresMDB.genre_movies(genreId: categoryNum[genre], include_adult_movies: true, language: "en") { [weak self] apiReturn, movieList in
guard let self = self else {return}
if let movies = movieList
{
for movie in movies
{
self.movies.append(MovieDT(title: movie.title ?? "Missing Title", description: movie.overview ?? " ", releaseDate: movie.release_date ?? " ", stars: movie.vote_average ?? 0, id: movie.id ?? 0))
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
//****************************************************************
//code below does not work because the movies aren't generated yet
//****************************************************************
for movie in self.movies
{
self.db.collection("Movies").document(String(movie.id)).getDocument() { [weak self] (querySnapshot, err) in
guard let self = self else {return}
if let err = err {
print("Error getting documents: \(err)")
} else {
guard let snap = querySnapshot else {return}
let data = snap.data()
print(data?["upvoteCount"] as? Int ?? 0)
print(data?["downvoteCount"] as? Int ?? 0)
print("jhsjsj")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}

Since GenresMDB.genre_moves is an asynchronous function, the callback will execute on a separate thread. Meanwhile, init continues executing on the main thread. The API call takes time to complete, so your for loop executes before you've had a chance to update movies. If you add a print right after the guard and another one right before for movie in self.movies, I suspect you'll see the first print after the second one, even though you wrote the first one first.
So, the solution is just to move your for loop into the completion block. I would merge it with your for movie in movies loop. The trick is to make your code execute synchronously and deterministically, to ensure that it will certainly run in the order that you hope.

Related

Swift Dispatch Group in function

thank you in advance. I am working with a UITableView and need an array to be created before loading the cells. I am attempting to use DispatchGroup, I was successful in letting the first array be created but a second array which I also need validPhonesNotUsingApp, in the same function is not created.
I am leaving parts of overall file out.
Thank you.
override func viewDidLoad() {
super.viewDidLoad()
let group = DispatchGroup()
setUpElements()
group.enter()
checkContacts(group)
group.notify(queue: .main){
self.tableView.dataSource = self
self.tableView.delegate = self
self.searchBar.delegate = self
self.tableView.keyboardDismissMode = .onDrag
print(self.validPhonesNotUsingApp)
self.tableView.register(TableViewCellForContacts.nib(), forCellReuseIdentifier: TableViewCellForContacts.identifier)
}
}
func checkContacts(_ group: DispatchGroup){
let db = Firestore.firestore()
db.collection("UserProfile").document(UserDataConst.UserUID).getDocument { (DocumentSnapshot1, Error1) in
if Error1 != nil{
print("Error finding if contacts uploaded")
group.leave()
}
else{
let hasContacts: Bool = DocumentSnapshot1?.get("Contacts Uploaded") as? Bool ?? false
if hasContacts == true{
db.collection("UserProfile").document(UserDataConst.UserUID).collection("ContactFriends").getDocuments { (
Snapshot2, Error2) in
if Error2 != nil{
group.leave()
return
}
else{
for x in 0..<Snapshot2!.documents.count{
group.enter()
let validNumber = self.correctPhoneNumber(Snapshot2!.documents[x].documentID, group)
if validNumber != nil{
self.validPhoneNumbers.append(validNumber!)
let first = Snapshot2!.documents[x].get("First Name") as? String ?? "(ø)"
self.validPhoneFirstName.append(first)
let last = Snapshot2!.documents[x].get("Last Name") as? String ?? "(ø)"
self.validPhoneLastName.append(last)
}
else{
group.leave()
}
}
db.collection("AllPhoneNumbers").getDocuments { (Snapshot3, Error3) in
if Error3 != nil{
group.leave()
return
}
else{
print("OK lemme know what sup")
let docs = Snapshot3!.documents
group.enter()
for x1 in 0..<self.validPhoneNumbers.count{
group.enter()
var found = false
for x2 in 0..<docs.count{
group.enter()
if self.validPhoneNumbers[x1] == docs[x2].documentID{
let uid = docs[x2].get("UID") as! String
db.collection("UserProfile").document(UserDataConst.UserUID).collection("Friends").getDocuments { (QuerySnapshot4, Error4) in
if Error4 != nil{
group.leave()
return
}
else if QuerySnapshot4!.documents.count != 0{
var found2 = false
for x3 in 0..<QuerySnapshot4!.documents.count{
group.enter()
if QuerySnapshot4!.documents[x3].documentID == uid{
found2 = true
//group.leave()
break
}
else{
group.leave()
}
}
if found2 == false{
self.UIDsUsingApp.append(uid)
}
}
else{
self.UIDsUsingApp.append(uid)
}
}
//self.UIDsUsingApp.append(uid)
found = true
//group.leave()
break
}
}
if found == false{
self.validPhonesNotUsingApp.append(self.validPhoneNumbers[x1])
self.validFirstNotUsingApp.append(self.validPhoneFirstName[x1])
self.validLastNotUsingApp.append(self.validPhoneLastName[x1])
group.leave()
}
print("OK now we getting activ")
}
//print(self.UIDsUsingApp)
}
}
}
}
}
else{
group.leave()
return
}
}
}
}
I am working with a UITableView and need an array to be created before loading the cells. I am attempting to use DispatchGroup
Well, don't. You cannot do anything "before loading the cells". Do not intermingle table handling with dispatch. And don't use a dispatch group in that way.
Everything about the table view must be done immediately and on the main queue. You register directly in viewDidLoad on the main queue. You return a cell immediately in cellForRowAt:. You do not "wait" with a dispatch group or in any other manner.
If you have data to gather for the table in a time-consuming way, fine; do that on a background queue, and update your data model, and then reload the table (on the main queue). So:
Initially, if the data is not ready yet, your data source methods find there is no data and they display an empty table.
Later, once you gather your data and tell the table view to reload, your data source methods find there is data and they display it.
A few observations:
You do not want to entangle the “completion handler” logic of checkContacts with dispatch groups you might be using within the function. If you ever find yourself passing dispatch group objects around, that’s generally a sign that you are unnecessarily entangling methods.
So, if you need dispatch group within checkContacts, fine, use that, but don’t encumber the caller with that. Just use the completion handler closure pattern.
Make sure that you are not updating your model objects until the asynchronous process is done.
For example:
func checkContacts(completion: #escaping (Result<[Contact], Error>) -> Void) {
let group = DispatchGroup()
var contacts: [Contact] = [] // in this method, we will only update this local variable
...
group.notify(queue: .main) {
if let error = error {
completion(.failure(error))
} else {
completion(.success(contacts)) // and when we’re done, we’ll report the results
}
}
}
And you’d call it like
checkContacts { results in
switch results {
case .failure(let error):
...
case .success(let contacts):
self.contacts = contacts // only now will we update model properties
... // do whatever UI updates you want, e.g.
self.tableView.reloadData()
}

How to rewrite the code in order to get rid of the duplicate codes in Swift?

I'm learning ios dev through making an app. I wirte code in Swift and for the backend, I use Firestore. This app is supposed to be the multiplayer tic tac toe.
Since two people access the same data, I store data in Firestore, but every time if I want to check inside of the data, I have to read the same document (which is ok).
In my Viewcontroller, I made several functions that all of them need to get the data from Firestore. So, everytime I have to fetch the same document with same code.
let docRef = db.collection(K.FStore.newGameCollection).document(gameDocumentID)
docRef.addSnapshotListener { (documentSnapshot, error) in
if let err = error {
print("Error getting documents: \(err)")
} else {
let gameDocData = documentSnapshot?.data()....
and some of the examples are
func changePlateImage (plate: UIButton) {
let docRef = db.collection(K.FStore.newGameCollection).document(gameDocumentID)
docRef.addSnapshotListener { (documentSnapshot, error) in
if let err = error {
print("Error getting documents: \(err)")
} else {
let gameDocData = documentSnapshot?.data()
let isPlayer1 = gameDocData?[K.FStore.isPlayer1Turn] as? Bool
let fruitImage = isPlayer1! ? K.Image.apple : K.Image.pineapple
plate.setImage(UIImage(named: fruitImage), for: .normal)
self.changeGameBoard(index: plate.tag, fruit: fruitImage)
}
}
}
and
func changeGameBoard (index: Int, fruit: String){
let docRef = db.collection(K.FStore.newGameCollection).document(gameDocumentID)
docRef.addSnapshotListener { (documentSnapshot, error) in
if let err = error {
print("Error getting documents: \(err)")
} else {
let gameDocData = documentSnapshot?.data()
var gameBoard = gameDocData?[K.FStore.gameBoardField] as? Array<String>
// add name of the fruit in the gameBoard
gameBoard?[index] = fruit
}
}
}
As I said before, every time before I run the functions I write the same code. If I need to write the same code in every function, is there any better way to refactor this function? or I should just make a huge function? Sorry for the stupid question.
What you can do is to write a function to get data.. and then manipulate it according to condition
func getGameDataFromDB (completion: #escaping(_ gameDocData: gameDocDataTypeHere?, _ error:Error?)->Void ) {
let docRef = db.collection(K.FStore.newGameCollection).document(gameDocumentID)
docRef.addSnapshotListener { (documentSnapshot, error) in
if let err = error {
completion(nil,err)
print("Error getting documents: \(err)")
} else {
completion(documentSnapshot?.data(),nil)
}
}
}
Use it like this
getGameDataFromDB { (data, error) in
var gameBoard = data?[K.FStore.gameBoardField] as? Array<String>
// add name of the fruit in the gameBoard
gameBoard?[index] = fruit
}

Dispatch Queue Async Call

I am firing off a network request inside a for loop that is within another network request. I'm using Core Data but I am fairly certain this is not a Core Data issue, and is an async issue.
The 2 print statements inside the the Firebase request print the data properly, but without the DispatchQueue the array returns as empty (before the network request completes).
Here's a picture of the crash:
Here is the code itself:
var userReps = [UserRepresentation]()
// Fetch all Friends -> update Core Data accordingly
func fetchFriendsFromServer(completion: #escaping (Error?) -> Void = { _ in}){
let backgroundContext = CoreDataStack.shared.container.newBackgroundContext()
// 1. Fetch all friends from Firebase
FirebaseDatabase.UserDatabaseReference.child(CoreUserController.shared.userPhoneNumber).child(UserKeys.UserFriends).child(UserKeys.UserAcceptedFriends).observe(.value) { (data) in
if let dictionary = data.value as? [String: String] {
var userReps = [UserRepresentation]()
let group = DispatchGroup()
group.enter()
for friend in dictionary {
let friendName = friend.value
let friendId = friend.key
FirebaseDatabase.UserDatabaseReference.child(friendId).observe(.value, with: { (data) in
if let dictionary = data.value as? [String: Any] {
guard let gender = dictionary[UserKeys.UserGender] as? String else {return}
guard let bio = dictionary[UserKeys.UserBio] as? String else {return}
guard let status = dictionary[UserKeys.UserStatus] as? String else {return}
guard let avatarUrl = dictionary[UserKeys.UserAvatarUrlKey] as? String else {return}
let friendRepresentation = UserRepresentation(avatarUrl: avatarUrl, name: friendName, number: friendId, gender: gender, status: status, bio: bio)
userReps.append(friendRepresentation)
print("HERE, friends fetched: ", friendRepresentation)
print("HERE, reps fetched: ", userReps)
group.leave()
}
})
}
group.notify(queue: .main) {
// 2. Update Core Data value with Firebase values
self.updateFriends(with: userReps, in: backgroundContext)
// 3. Save Core Data background context
do {
try CoreDataStack.shared.save(context: backgroundContext)
} catch let error {
print("HERE. Error saving changes to core data: \(error.localizedDescription)")
}
}
}
}
}
Any help would go a long way
Since
let group = DispatchGroup()
is a local variable and you use observe here
FirebaseDatabase.UserDatabaseReference.child(friendId).observe(.value, with: { (data) in
it will re-call it after function deallocation either make it an instance variable or make this single observe
FirebaseDatabase.UserDatabaseReference.child(friendId).observeSingleEvent(of:.value) { (data) in
Also make enter inside the for loop
for friend in dictionary {
group.enter()

How do I pull this value out of a closure to update a cutom Object in Swift?

I am building am application with a Firestore back end, and I am trying to call a document down with the current user's info for their settings page. I am able to do this no problems when updating an empty array, but am having a terrible time trying to populate a single document. I have a custom object called DxUser:
struct DxUser {
var email:String?
var name:String?
var timeStamp:Date?
var profileImageURL:String?
var ehr: String?
var dictionary:[String:Any]? {
return [
"email":email,
"name":name,
"timeStamp":timeStamp,
"profileImageURL":profileImageURL,
"ehr": ehr as Any
]
}
}
extension DxUser : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let email = dictionary["email"] as? String,
let name = dictionary["name"] as? String,
let timeStamp = dictionary["timeStamp"] as? Date,
let profileImageURL = dictionary["profileImageURL"] as? String,
let ehr = dictionary["ehr"] as? String else {return nil}
self.init(email: email, name: name, timeStamp: timeStamp, profileImageURL: profileImageURL, ehr: ehr)
}
}
In the view controller, I am trying to update this variable with the current user, but I can only grab it in the closure, and it populates as nil anywhere outside the block. Here is the basic code I am using, but can anyone tell me what I am missing?
class SettingsController: UIViewController {
var dxUser = DxUser()
override func viewDidLoad() {
super.viewDidLoad()
fetchUser()
}
func fetchUser() {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let userRef = db.collection("users").document(uid)
userRef.getDocument { (document, error) in
if error != nil {
print(error as Any)
} else {
self.dxUser = DxUser(dictionary: (document?.data())!)!
self.navigationItem.title = self.dxUser.name
print (self.dxUser)
}
}
}
Yeah, this is how I am doing it on the table view, but I didn't see anything comparable on the regular VC.
db.collection("users").document(uid!).collection("goals").getDocuments { (snapshot, error) in
if error != nil {
print(error as Any)
} else {
//set the profile array to equal whatever I am querying below
goalsArray = snapshot!.documents.flatMap({Goal(dictionary: $0.data())})
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
}
}
It's not so much about the location of where you can access dxUser. It's the timing. Firebase APIs are asynchronous, which means userRef.getDocument() returns immediately, and you receive a callback only after the request completes. If you try to access dxUser right after that call within your fetchUser() method, it will not be available, because the request isn't complete. Given that's how async APIs work, you should only work with dxUser after the callback invoked, which usually means delaying the rendering of that data in your app (such as where your print statement is).
Please read more here about why Firebase APIs are asynchronous and what you can expect from them.
I actually figured it out. I pulled the fetchUser() function out to the viewWillAppear function so I called it before the view appeared. Then I did the async function to re-run viewDidLoad. Anyone else have any better suggestions?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
fetchUser()
}
func fetchUser() {
let userRef = db.collection("users").document(uid)
userRef.getDocument { (document, error) in
if error != nil {
print(error as Any)
} else {
self.dxUser = DxUser(dictionary: (document?.data())!)!
DispatchQueue.main.async {
self.viewDidLoad()
}
self.navigationItem.title = self.dxUser.name
}
}
}

How to set up DispatchGroup in asynchronous iteration?

I´m trying to set up an iteration for downloading images. The whole process works, but taking a look in the console´s output, something seems to be wrong.
func download() {
let logos = [Logos]()
let group = DispatchGroup()
logos.forEach { logo in
print("enter")
group.enter()
if logo?.data == nil {
let id = logo?.id as! String
if let checkedUrl = URL(string: "http://www.apple.com/euro/ios/ios8/a/generic/images/\(id).png") {
print(checkedUrl)
LogoRequest.init().downloadImage(url: checkedUrl) { (data) in
logo?.data = data
print("stored")
group.leave()
print("leave")
}
}
}
}
print("loop finished")
}
Output:
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/og.png
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/eg.png
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/sd.png
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/hd.png
loop finished
stored
leave
stored
leave
stored
leave
stored
leave
It looks like the iteration does not care about entering and leaving the DispatchGroup() at all. The webrequests are fired almost at the same time. In my opinion the output should look like this:
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/og.png
stored
leave
enter
http://www.apple.com/euro/ios/ios8/a/generic/images/eg.png
stored
leave
...
loop finished
Did I oversee something? Would be awesome to get some ideas.
What about this:
group.notify(queue: .main) {
print("loop finished")
}
Instead of your normal print.
edit:
func download() {
let logos = [Logos]() // NSManagedObject
let group = DispatchGroup()
logos.forEach { logo in
if logo?.data == nil {
let id = logo?.id as! String
if let checkedUrl = URL(string: "http://www.apple.com/euro/ios/ios8/a/generic/images/\(id).png") {
print(checkedUrl)
print("enter")
group.enter()
LogoRequest.init().downloadImage(url: checkedUrl) { (data) in
//this is async I think
coin?.logo = data
print("stored")
group.leave()
print("leave")
}
}
}
}
group.notify(queue: .main) {
print("loop finished")
}
}