0

I am having a hard time updating an array in my app that controls a table. I want to be able to change a child node in Firebase Database and have it update on my users' end without me having to code and send an update. I need to be able to change their tag and have it update the table without the index being out of range.

Here is my Class Struct for the information for the users:

import UIKit

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

}

In my controller I have the following code to initialize the table with the info from Firebase:

func CreateDataArray() {
    
    Database.database().reference().child("Users").observe(.childAdded, with: { (snapshot) in
        
        if let dictionary = snapshot.value as? [String: AnyObject] {
            
            let user = User()
            //self.setValuesForKeys(dictionary)
            user.Name = dictionary["Name"] as? String
            user.Email = dictionary["Email"] as? String
            user.UID = dictionary["UID"] as? String
            user.Tag = dictionary["Tag"] as? String
            
            if user.Tag == "Macro" {
                            self.macroUsersArray.append(user)
            } else if user.Tag == "Micro" {
                            self.microUsersArray.append(user)
                        }
            
            self.users.append(user)
            self.userName.append(user.Name!)
            self.filteredNames.append(user.Name!)
            
            print(dictionary)
            print(self.macroUsersArray)
            
            DispatchQueue.main.async {
            self.tableView.reloadData()
                }
            }
        }, withCancel: nil)
    
    }

I then have a function that listens for any changes from my Firebase and updates the arrays:

func updateDataArray() {
    
    Database.database().reference().child("Users").observe(.childChanged, with: { (snapshot) in
        
        if let dictionary = snapshot.value as? [String: AnyObject] {
            
            let user = User()
            //self.setValuesForKeys(dictionary)
            user.Name = dictionary["Name"] as? String
            user.Email = dictionary["Email"] as? String
            user.UID = dictionary["UID"] as? String
            user.Tag = dictionary["Tag"] as? String
            
            if user.Tag == "Macro" {
                self.microUsersArray.remove(at: 2)
            } else if user.Tag == "Micro" {
                self.macroUsersArray.remove(at: 2)
                        }
            
            self.users.append(user)
            self.userName.append(user.Name!)
            self.filteredNames.append(user.Name!)
            
            print(dictionary)
            print(self.macroUsersArray)
            
            DispatchQueue.main.async {
            self.tableView.reloadData()
                }
            }
        }, withCancel: nil)
    
    }

I want to get the user that originally had the tag "macro" that got switch to "micro" to be removed from the macro list and show up in the micro list. I am not able to get the arrays to append and keep getting the indexPath is out of range error that crashes my app. I really need help and have been struggling with this the last few weeks.

Edit: I have decided to use observe.value so that when I make changes it reads it. Here is my code for creating the users' info:

func CreateDataArray() {
    
    Database.database().reference().child("Users").observe(.value, with: { (snapshot) in
        
        if let dictionary = snapshot.value as? [String: AnyObject] {
            
            let user = User()
            //self.setValuesForKeys(dictionary)
            user.Name = dictionary["Name"] as? String
            user.Email = dictionary["Email"] as? String
            user.Tag = dictionary["Tag"] as? String
            
            if user.Tag == "Macro" {
                            self.macroUsersArray.append(user)
            } else if user.Tag == "Micro" {
                            self.microUsersArray.append(user)
                        }
            
            self.users.append(user)
            self.userName.append(user.Name!)
            self.filteredNames.append(user.Name!)
            
            print(dictionary)
            print(self.macroUsersArray)
            
            DispatchQueue.main.async {
            self.tableView.reloadData()
                }
            }
        }, withCancel: nil)
    
    }

However, I am able to pull all the data into a snapshot but it is returning nil when I try to append. I am getting an error of nil on any line of code with "append(User.Name!)

I would love to figure out how to stop this nil and allow my observer to keep listening for changes in my database.

EDIT 2:

Here are my variables:

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

Here is my ViewDidLoad:

 override func viewDidLoad() {
        super.viewDidLoad()
        
        updateDataArray()
        
    }

Here is my updateDataArray code:

func updateDataArray() {
    
    Database.database().reference().child("Users").observe(.childAdded, with: { (snapshot) in
        
        if let dictionary = snapshot.value as? [String: AnyObject] {
            
            let user = User()
            //self.setValuesForKeys(dictionary)
            user.Name = dictionary["Name"] as? String
            user.Email = dictionary["Email"] as? String
            user.UID = dictionary["UID"] as? String
            user.Tag = dictionary["Tag"] as? String
            
            self.allUsersArray.append(user)
            self.users.append(user)
            self.userName.append(user.Name!)
            self.filteredNames.append(user.Name!)

            self.macroUsersArray = self.allUsersArray.filter { $0.Tag == "Macro" }
            self.microUsersArray = self.allUsersArray.filter { $0.Tag == "Micro" }
            self.observeChangeInUserProperty()
            
            DispatchQueue.main.async {
            self.tableView.reloadData()
                }
            }
        }, withCancel: nil)
    
    }

Here is the observeChangeInUserProperty() function:

func observeChangeInUserProperty() {
    let ref = Database.database().reference().child("Users").child("Talent")
    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.microTableView.reloadData()
                self.tableView.reloadData()
                
            }
        }
    })
}

I am getting empty arrays for Macro and Micro. I don't know what I am doing wrong.

EDIT 3:

Here is my new user struct with initialization from Firebase:

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

        }
    }

I am now able to append the arrays but they are not populating my tables. I can also change the user's tag in Firebase without the app crashing but it doesn't observe the changed Tag and move the user to the other array. So I need help with those two things.

NewCoder
  • 35
  • 6

1 Answers1

1

This answer is going to require a little set up and restatement of the goal

The goal is 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) the OP wants to remove the user from one table and add it to the other.

the conceptual answer is: add an observer to the users node in Firebase and when a change occurs to a user, see if it's the type property, and if so remove from one table and add it to another.

There are lots of solutions but let me present this very long-winded answer which could be reduced in both code and by property observers.

Start with three arrays, one to hold all users and the others to hold macro and micro users. Assume the UserClass has a String type property of either macro or micro

var allUsersArray = [UserClass]()
var macroArray = [UserClass]()
var microArray = [UserClass]()

Read in all users and then from that, divide the users into their respective type

...read firebase single event of .value, with: { snapshot in
   and populate allUsersArray from the snapshot..
   self.macroArray = self.allUsersArray.filter { $0.type == "macro" }
   self.microArray = self.allUsersArray.filter { $0.type == "micro" }
   self.observeChangeInUserProperty()
}

The below code adds an observer to the users array which will notify the app when any property of a user changes, and presents that user node in the snapshot.

func observeChangeInUserProperty() {
    let ref = self.ref.child("users")
    ref.observe(.childChanged, with: { snapshot in
        let key = snapshot.key
        let type = snapshot.childSnapshot(forPath: "type").value as! String // ! = never optional
        //get the user from the allUsersArray by its key
        if let user = self.allUsersArray.first(where: { $0.key == key }) {
            if user.type != type { //if the type changed, handle it
                user.type = type //update the allUsersArray
                if type == "macro" { //if the new type is macro remove the user from the micro array
                    if let userIndex = self.microArray.firstIndex(where: { $0.key == key }) {
                        self.microArray.remove(at: userIndex)
                        self.macroArray.append(user) //add user to macro array
                    }
                } else { //new type is micro so remove from macro array
                    if let userIndex = self.macroArray.firstIndex(where: { $0.key == key }) {
                        self.macroArray.remove(at: userIndex)
                        self.microArray.append(user)
                    }
                }
                //reload the tableviews to reflect the changes
                self.microTable.reloadData()
                self.mactoTable.reloadData()
            }
        }
    })
}

As mentioned this is a lot of extraneous code and could be condensed with property observers or leveraging a single array for both tables or a number of other solutions.

Edit

In case you want to know how to populate the all users array from Firebase, you need to have a User class that is init'ed from a snapshot and here's the code

func loadAllUsersAndPopulateArray() {
    let ref = self.ref.child("users")
    ref.observeSingleEvent(of: .value, with: { snapshot in
        let allUsersSnapshot = snapshot.children.allObjects as! [DataSnapshot]
        for userSnap in allUsersSnapshot {
            let user = UserClass(withSnap: userSnap)
            self.allUsersArray.append(user)
        }
    })
}
Jay
  • 34,438
  • 18
  • 52
  • 81
  • Hey Jay. I updated my question with an Edit 2 that shows how I took your code and adjusted it to my project but I am still having problems. – NewCoder Nov 29 '20 at 21:19
  • @NewCoder Your going to have problems with that code because Firebase is asynchronous and the code in the closure will only execute once the data is returned from Firebase. Meaning that this code `self.macroUsersArray = self.allUsersArray.filter` will run *before* Firebase has populated the allUsersArray. Code is faster than the internet so you need to populate those arrays within the closure, after the allUsers array has been populated. See the code in my answer. – Jay Nov 30 '20 at 18:25
  • I added that code and calling the function to update child within the updateDataArray function and I am still getting empty macro and micro arrays. What should I try next? – NewCoder Nov 30 '20 at 23:34
  • @NewCoder The issues still exists. In the `viewDidLoad` your calling `updateDataArray()` and the line right after that is `self.macroUsersArray = self.allUsersArray.filter` so that line will execute **before** the code in the Firebase closure within `updateDataArray()` so the `allUsersArray` will be empty. You need to code asynchronously. See my answers [here](https://stackoverflow.com/questions/56025373/read-data-firebase-assign-value/56062986#56062986) and [here](https://stackoverflow.com/questions/59588174/code-not-executing-after-observe-firebase-method-swift4/59592474#59592474) – Jay Nov 30 '20 at 23:51
  • Hey Jay. I'm sorry I didn't make myself clear. Please see the edit made in edit two. I moved the = self.allUsersArray.filter to the updateDataArray(). – NewCoder Dec 01 '20 at 03:02
  • @NewCoder Somewhat of the same issue but now you've got additional problems because you're still using .childAdded (not .value as in my answer). .childAdded iterates over every child node, one at a time. So now this code `self.allUsersArray.filter` repeats as well as this code `self.observeChangeInUserProperty()` So if you have 1000 users, both of those are called 1000 times - that's a LOT of added observers. You're also calling this `self.tableView.reloadData()` 1000 times. As a side note, UI calls are handled on the main thread within Firebase closures so you don't need `DispatchQueue` – Jay Dec 01 '20 at 19:06
  • I updated the code to be .value instead of .childAdded and now my allUsersArray is returning nil. How do I get the .value to input all the child values under users into my User Struct so I can add it to the arrays? – NewCoder Dec 01 '20 at 23:46
  • When I print(user), I am getting the following result in my terminal: . I think this is why I am not able to append and filter the proper tags to separate users since strings are being saved to the array. – NewCoder Dec 02 '20 at 05:52
  • @NewCoder I hate to just refer you to my answer but that's the code you need. It works and directly from a functional project. – Jay Dec 02 '20 at 18:49
  • I'm sorry to keep bugging you Jay but how would you go about reading firebase and appending the allUserArray? Could show me? I hope this isn't an extreme ask. Thank you again for taking time out of your day to help me. – NewCoder Dec 02 '20 at 23:20
  • Hey Jay. I'm sorry to keep adding comments but I am able to get the initialization and append the micro and macro arrays using your code with struct code in my 3rd edit. I am not getting any errors or crashing but the arrays themselves just aren't displaying my users. – NewCoder Dec 03 '20 at 20:27
  • @NewCoder That question goes beyond the context of the original question. If you have questions and need assistance with working with tableViews (and a dataSource in this case), I would suggest adding another specific question about that topic. Since the initial question has been resolved with my answer it should be accepted so this topic can help others in the future. – Jay Dec 03 '20 at 23:46
  • Here is the new question in this saga: https://stackoverflow.com/questions/65136645/change-in-firebase-doesnt-refresh-table-tables-not-displaying-user-nodes-from – NewCoder Dec 04 '20 at 01:33