0

I am trying to have two arrays, used as dataSources for two tableViews. One array contains user type: macro and the other array contains user type: micro.

If a user type is changed from macro to micro (and vice-versa) I want to remove the user from one table and add it to the other.

Currently I am able to update the user's tag if changed in Firebase Database and have it appear in the proper array when the app is restarted. The observe function only collects it once and if it is changed, it doesn't update the table until the user quits the app and reopens it. The function I am using to observe.childChanged doesn't seem to update the arrays immediately on the user's app unless they do what was mentioned previously.

My main problem is that my tables are not displaying the users to the table. I am able to access their node and user but they are not appearing in my tables.

Here is the code for my user class:

import UIKit
import Firebase

    class User: NSObject {
        var Name: String?
        var Email: String?
        var UID: String?
        var Tag: String?

init?(from snapshot: DataSnapshot) {

        let dictionary = snapshot.value as? [String: Any]
    
        self.Name = dictionary!["Name"] as? String
        self.Email = dictionary!["Email"] as? String
        self.UID = dictionary!["UID"] as? String
        self.Tag = dictionary!["Tag"] as? String
        
        }
}

Here is my code for loading and populating my allUsersArray the info appends:

func loadAllUsersAndPopulateArray() {
    let ref = Database.database().reference().child("Users")
    ref.observeSingleEvent(of: .value, with: { snapshot in
        let allUsersSnapshot = snapshot.children.allObjects as! [DataSnapshot]
        for userSnap in allUsersSnapshot {
            let user = User(from: userSnap)
            self.allUsersArray.append(user!)
            
            self.macroUsersArray = self.allUsersArray.filter { $0.Tag == "Macro" }
            self.microUsersArray = self.allUsersArray.filter { $0.Tag == "Micro" }

            self.observeChangeInUserProperty()
        }
    })
}

Here is my code for observing the change from Firebase:

 func observeChangeInUserProperty() {
    let ref = Database.database().reference().child("Users")
    ref.observe(.childChanged, with: { snapshot in
        let key = snapshot.key

        let tag = snapshot.childSnapshot(forPath: "Tag").value as! String // ! = never optional
        //get the user from the allUsersArray by its key
        if let user = self.allUsersArray.first(where: { $0.Tag == key }) {
            if user.Tag != tag { //if the tag changed, handle it
                user.Tag = tag //update the allUsersArray
                if tag == "Macro" { //if the new tag is Macro remove the user from the Micro array
                    if let userIndex = self.microUsersArray.firstIndex(where: { $0.Tag == key }) {
                        self.microUsersArray.remove(at: userIndex)
                        self.macroUsersArray.append(user) //add user to macro array
                    }
                } else { //new type is micro so remove from macro array
                    if let userIndex = self.macroUsersArray.firstIndex(where: { $0.Tag == key }) {
                        self.macroUsersArray.remove(at: userIndex)
                        self.microUsersArray.append(user)
                    }
                }
                //reload the tableviews to reflect the changes
                self.tableView.reloadData()
                self.microTableView.reloadData()

            }
        }
    })
}

Here is my code for the controller:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

var allUsersArray = [User]()
var macroUsersArray = [User]()
var microUsersArray = [User]()

override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
        searchBar.delegate = self
        tableView.register(UserCell.self, forCellReuseIdentifier: networkCell)
        
        microTableView.delegate = self
        microTableView.dataSource = self
        microTableView.register(microCell.self, forCellReuseIdentifier: microCell)
        
        loadAllUsersAndPopulateArray()
        
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        if (tableView === self.tableView) {
        
            return 1
            
          }


        else if (tableView === self.microTableView) {
            // Do something else
        
            return 1
        
        }
        
        fatalError("Invalid table")
        
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        if (tableView === self.tableView) {
            
                let cell = tableView.dequeueReusableCell(withIdentifier: networkCell, for: indexPath) as! UserCell
            
                let user = macroUsersArray[indexPath.row]
                cell.textLabel?.text = user.Name
                    
                    return cell
                    
                } else if (tableView === self.microTableView) {
                    
                    let cell = tableView.dequeueReusableCell(withIdentifier: microCell, for: indexPath) as! microInfluencerCell
                
                    let user = microUsersArray[indexPath.row]
                    cell.textLabel?.text = user.Name
                
                        return cell
            
        } else {
            fatalError("Invalid table")
        }
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        if (tableView === self.tableView) {
            return macroUsersArray.count
        }
        else if (tableView === self.microTableView) {
            return microUsersArray.count
        }
        
        else {
            fatalError("Invalid table")
        }

    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        
        if (tableView === self.tableView) {
            return 72
        }
        else if (tableView === self.microTableView) {
            return 72
        }
        
        else {
            fatalError("Invalid table")
        }
    }

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
    if (tableView === self.tableView) {
        
        dismiss(animated: true) {
            let userName = self.macroUsersArray[indexPath.row]
            self.showSecondViewController(user: userName)

            print("Dismiss completed")
        }
        
    }
    else if (tableView === self.microTableView) {
        
        dismiss(animated: true) {
            let userName = self.microUsersArray[indexPath.row]
            self.showSecondController(user: userName)

            print("Dismiss completed")
        }
        
    }
    
    else {
        fatalError("Invalid table")
    }
  }
}

func showSecondViewController(user: User) {

    let SecondViewController = SecondViewController()
    SecondViewController.user = user
    let navigationController = UINavigationController(rootViewController: SecondViewController)
    self.present(navigationController, animated: true, completion: nil)
  }

func loadAllUsersAndPopulateArray() {
let ref = Database.database().reference().child("Users")
ref.observeSingleEvent(of: .value, with: { snapshot in
    let allUsersSnapshot = snapshot.children.allObjects as! [DataSnapshot]
    for userSnap in allUsersSnapshot {
        let user = User(from: userSnap)
        self.allUsersArray.append(user!)

        self.macroUsersArray = self.allUsersArray.filter { $0.Tag == "Macro" }
        self.microUsersArray = self.allUsersArray.filter { $0.Tag == "Micro" }

        self.observeChangeInUserProperty()
     }
   })
 }
func observeChangeInUserProperty() {
let ref = Database.database().reference().child("Users")
ref.observe(.childChanged, with: { snapshot in
    let key = snapshot.key

    let tag = snapshot.childSnapshot(forPath: "Tag").value as! String // ! = never optional
    //get the user from the allUsersArray by its key
    if let user = self.allUsersArray.first(where: { $0.Tag == key }) {
        if user.Tag != tag { //if the tag changed, handle it
            user.Tag = tag //update the allUsersArray
            if tag == "Macro" { //if the new tag is Macro remove the user from the Micro array
                if let userIndex = self.microUsersArray.firstIndex(where: { $0.Tag == key }) {
                    self.microUsersArray.remove(at: userIndex)
                    self.macroUsersArray.append(user) //add user to macro array
                }
            } else { //new type is micro so remove from macro array
                if let userIndex = self.macroUsersArray.firstIndex(where: { $0.Tag == key }) {
                    self.macroUsersArray.remove(at: userIndex)
                    self.microUsersArray.append(user)
                }
            }
            //reload the tableviews to reflect the changes
            self.tableView.reloadData()
            self.microTableView.reloadData()

        }
       }
    })
  }
}

I am so close to getting this problem solved. Any help would be amazing.

EDIT:

func loadAllUsersAndPopulateArray() {
    let ref = Database.database().reference().child("Users").child("Talent")
    ref.observeSingleEvent(of: .value, with: { snapshot in
        let allUsersSnapshot = snapshot.children.allObjects as! [DataSnapshot]
        for userSnap in allUsersSnapshot {
            let user = User(from: userSnap)
            self.allUsersArray.append(user!)
            self.allUsersNames.append(user!.Name!)

        }

        self.observeChangeInUserProperty()
    })

    self.macroUsersArray = self.allUsersArray.filter { $0.Tag == "Macro" }
    self.microUsersArray = self.allUsersArray.filter { $0.Tag == "Micro" }
    
    self.tableView.reloadData()
    self.microTableView.reloadData()
    
}
NewCoder
  • 35
  • 6
  • You've got code in the wrong place. When populating the allUsersArray `for userSnap in allUsersSnapshot` you don't need to keep calling `self.macroUsersArray` or `self.observeChange` over and over for every user. Populate the allUsersArray first, then immediately *after that loop* populate the macro and micro sub arrays and add the observer. And as a troubleshooting suggestion, eliminate Firebase from the equation temporarily and create some dummy test data as your tableView datasource(s) to ensure you tableViews and cells are actually working correctly. – Jay Dec 04 '20 at 20:33
  • I fixed the code but my tables' codes work fine when I have other data. I don't understand why the tables aren't populating. – NewCoder Dec 04 '20 at 22:38
  • You really need to do some troubleshooting to see what's not right. Add breakpoints and step through your code - see if `func tableView(_ tableView: UITableView, numberOfRowsInSection` is returning the correct # of rows. Then check `func tableView(_ tableView: UITableView, cellForRowAt` and see if the cell is being populated with valid strings etc. – Jay Dec 04 '20 at 22:57
  • So I did some trouble shooting and outside of the loadAllUsers func, the allUsersArray isn't appended. I checked within the snapshot but outside the function the arrays are empty – NewCoder Dec 04 '20 at 23:19
  • You should update the code in the question in the `loadAllUsersAndPopulateArray` function per my above comment as it's still not correct. As far as `allUsersArray` being empty outside that function, you've got a mystery on your hands. It would only be empty if the viewController went out of scope, was called multiple times (so your pointing to a different instance than you thought you were) or if you're removing the contents. I suspect it may have something to do with the viewController as calling this `var allUsersArray = [User]()` creates an empty array. Add breakpoints, walk through code. – Jay Dec 05 '20 at 15:30
  • @Jay Merry Christmas/Happy Holidays. I trouble shooted and figured out where I am receiving the problem. I am having the problem with the problem with the loadAllUsersAndPopulateArray. Whenever I print(self.allUsersArray) from within the ref.observeSingleEvent{} within the loadAllUsersAndPopulateArray(), I can see the list of users. When I move the print(self.allUsersArray) just within the func loadAllUsersAndPopulateArray(), I get an empty array. When I print the arrays in the viewDidLoad() I also get empty arrays. – NewCoder Dec 27 '20 at 21:44
  • You're observation is correct; Firebase is asynchronous and the Firebase data is only valid within the closure following the Firebase function. Any other code that attempts to access that data within viewDidLoad for example will execute before the data has been retrieved from Firebase. See Franks answer [here](https://stackoverflow.com/questions/65447695/how-do-you-force-the-viewcontroller-to-export-the-data-in-b-instead-of-a-to/65449973#65449973) with a bunch of links that discusses using asynchronous functions. – Jay Dec 28 '20 at 18:05
  • Hey @Jay. Thank you for the link. I read those links and I am trying to figure out how to get the arrays to be permanently appended outside of the closure and I seem to be running into a wall. – NewCoder Dec 28 '20 at 19:39
  • In `loadAllUsersAndPopulateArray`, right after this `for userSnap in allUsersSnapshot` loop, reload your tableViews. Also you're calling this `self.macroUsersArray = self.allUsersArray.filter { $0.Tag == "Macro" }` over and over and there's no reason to do that. Also add that right after the loop and before you reload your tableViews. Same thing with `self.observeChangeInUserProperty()`, there's no reason to repeatedly call it within the loop. Do it after the loop. – Jay Dec 28 '20 at 19:59
  • @Jay I edited the question with your suggestion and it is still giving me empty arrays – NewCoder Dec 28 '20 at 20:08
  • No, you didn't follow my instructions. You moved those calls **OUTSIDE** of the closure so they are called *before* the firebase data is populated. I said to put them **right after the for loop**, which would keep the code inside the closure where the firebase data is available. – Jay Dec 28 '20 at 20:12

2 Answers2

0

With the help from Jay I was able to figure out the solution!

I needed to be setting my arrays info from within the closure. Whenever a person's tag is updated, the app needs to be closed and reopened to show the new change made in firebase.

Thank you @Jay for all the help!!!

here is the new function code:

func loadAllUsersAndPopulateArray() {
    let ref = Database.database().reference().child("Users")
    ref.observeSingleEvent(of: .value, with: { snapshot in
        let allUsersSnapshot = snapshot.children.allObjects as! [DataSnapshot]
        for userSnap in allUsersSnapshot {
            let user = User(from: userSnap)
            self.allUsersArray.append(user!)
            self.allUsersNames.append(user!.Name!)

            self.macroUsersArray = self.allUsersArray.filter { $0.Tag == "Macro" }
            self.microUsersArray = self.allUsersArray.filter { $0.Tag == "Micro" }

            self.tableView.reloadData()
            self.microTableView.reloadData()
            
        }

        self.observeChangeInUserProperty()
    })
    
NewCoder
  • 35
  • 6
  • You should modify this to match the instructions I provided in a comment. As is, for every user you're re-assigning and filtering the array over and over. That's a lot of extra wasted CPU cycles. Along with that reloading the tableView repeatedly will cause it to flicker. Move this `self.macroUsersArray` and `self.microUsersArray` along with the tableView reloading after the for loop. – Jay Dec 28 '20 at 21:00
0

The issue you're running into is that Firebase is asynchronous - firebase data is only valid within the closure following the Firebase function.

Code after that closure will execute before the code within the closure so you need to plan to handle that data in an asynchronous way.

Additionally, since you're populating an array with data from firebase and the other arrays as based on the first, there's no reason to filter them over and over; populate the main array and then when that's done, populate the other arrays.

Lastly, tableViews are sensitive and loading them over and over may cause flicker. Again, populate your main array and once that's done and the other arrays as populated, reload the tableViews - once.

Here's how that section of code should look

func loadAllUsersAndPopulateArray() {
    let ref = Database.database().reference().child("Users")
    ref.observeSingleEvent(of: .value, with: { snapshot in
        let allUsersSnapshot = snapshot.children.allObjects as! [DataSnapshot]
        for userSnap in allUsersSnapshot {
            let user = User(from: userSnap)
            self.allUsersArray.append(user!)
            self.allUsersNames.append(user!.Name!)            
        }

        self.macroUsersArray = self.allUsersArray.filter { $0.Tag == "Macro" }
        self.microUsersArray = self.allUsersArray.filter { $0.Tag == "Micro" }

        self.tableView.reloadData()
        self.microTableView.reloadData()

        self.observeChangeInUserProperty()
    })
}

I will also suggest getting rid of the allUsersNames array since those names are also stored in the allUsersArray.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • unfortunately when I move the filters and table.reloadData() outside of where I have it, the tables do not populate and the arrays remain empty. When I have it set up with your code, my arrays remain empty and the table doesn't populate with my users. – NewCoder Dec 29 '20 at 03:59
  • @NewCoder Then you have something else wrong *or* this `(tableView === self.tableView)` is causing an issue as it's probably unnecessary. Actually it doesn't match up with your other delegate functions so you may want to re-think how those are coded. – Jay Dec 29 '20 at 16:30