Swift 3 and Firebase - I have successfully managed to fetch users' usernames and email addresses in tableview cells. However, when a user changes his username on a separate view controller, the tableview is not updating - the old username is still shown. Please have a look at the code below:
databaseRef.child("users").queryOrdered(byChild: "username").observe(.childAdded, with: { (snapshot) in
let key = snapshot.key
let snapshot = snapshot.value as? NSDictionary
snapshot?.setValue(key, forKey: "uid")
if(key == self.loggedInUser?.uid)
{
// don't add signed in user to array
}
else
{
var theUser = User()
theUser.key = key
theUser.fullname = snapshot?.value(forKey: "fullname") as? String
theUser.biography = snapshot?.value(forKey: "biography") as? String
theUser.location = snapshot?.value(forKey: "location") as? String
theUser.photoURL = snapshot?.value(forKey: "photourl") as? String
theUser.username = snapshot?.value(forKey: "username") as? String
self.arrayOfUsers.append(theUser)
//insert the rows
self.SearchUsersTableViewController.insertRows(at: [IndexPath(row:self.arrayOfUsers.count-1,section:0)], with: UITableViewRowAnimation.automatic)
self.tableView.reloadData()
}
})
I have tried placing tableview.reloadData() on multiple places without success. I have also tried using:
DispatchQueue.main.async {
self.tableView.reloadData()
}
without success. I came up with an idea to append the user again when he changes his username. However, I didn't know how to delete the old one. I thought the best solution was to add observe of type childChanged. The problem was that I couldn't find the index of the user who changed his username in users array.
I would be grateful if someone helps me solve the problem.
EDIT:
I have a struct User:
struct User {
var username: String?
var photoURL: String?
var biography: String?
var fullname: String?
var location: String?
var key: String?
}
For .childChanged I have used the code that Priyamal suggested:
databaseRef.observe(.childChanged, with: { snapshot in
let ID = snapshot.key //this is the firebaseKey
if let index = self.arrayOfUsers.index(where: {$0.key == ID}) {
let changedPost = self.arrayOfUsers[index]
//update the values
self.tableView.reloadData()
print("Change!")
}
})
However, when I change the username, I never get to the "Change!" output in my console; therefore, the tableview is not changed.
i think you need to change the event type from childAdded to childChanged
with child changed you will only get the updated value. then you must update the existing element in your array.
let's assume your User Struct looks like this
struct User {
var keyID : String?
var name : String?
}
var userArray = [User]() //this represents the array holding user objects
this method will get called if an update happens
databaseRef.observeEventType(.ChildChanged, withBlock: { snapshot in
let ID = snapshot.key //this is the firebaseKey
if let index = self. userArray.indexOf({$0.keyID == ID}) {
let changedPost = self. userArray[index]
//update the values
self.tableView.reloadData
}
to load UserArray at the first place use this method.
databaseRef.child("users").queryOrdered(byChild: "username").observe(.value, with: { (snapshot) in
let key = snapshot.key
let snapshot = snapshot.value as? NSDictionary
snapshot?.setValue(key, forKey: "uid")
if(key == self.loggedInUser?.uid)
{
print("Should not be shown!")
}
else
{
self.usersArray.append(snapshot)
self.SearchUsersTableViewController.insertRows(at: [IndexPath(row:self.usersArray.count-1,section:0)], with: UITableViewRowAnimation.automatic)
self.tableView.reloadData()
}
})
I have successfully solved the problem. After I got the index, I only needed to update the user at that specific location in the array and refresh that particular row in the table view!
databaseRef.child("users").queryOrdered(byChild: "username").observe(.childChanged, with: { (snapshot) in
let ID = snapshot.key
if let index = self.arrayOfUsers.index(where: {$0.key == ID}) {
let value = snapshot.value as? NSDictionary
self.arrayOfUsers[index].username = value?["username"] as? String
let indexPath = IndexPath(item: index, section: 0)
self.tableView.reloadRows(at: [indexPath], with: .top)
}
})
Related
In order to populate my tableView, I append items (created from a struct) to a local array:
func loadList() {
var newAnnotations: [AnnotationListItem] = []
if let uid = Auth.auth().currentUser?.uid {
uidRef.child(uid).child("annotations").queryOrderedByKey().observeSingleEvent(of: .value, with: {snapshot in
for item in snapshot.children {
let annotationItem = AnnotationListItem(snapshot: item as! DataSnapshot)
newAnnotations.append(annotationItem)
}
annotationList = newAnnotations
self.tableView.reloadSections([0], with: .fade)
})
}
}
When I click a specific row, I am taken to a DetailViewController where it is only a large UITextView (named notes). The UITextView.text displayed is based on the selected indexPath.row and the "notes" value is retrieved from the array. Now the user is able to type some text and when they are done, the textViewDidEndEditing function is called:
func textViewDidEndEditing(_ textView: UITextView) {
notes.resignFirstResponder()
navigationItem.rightBarButtonItem = nil
let newNotes = self.notes.text
print(newNotes!)
}
Now I'd like to updateChildValues to newNotes to the child node "notes" in my JSON:
"users" : {
"gI5dKGOX7NZ5UBqeTdtu30Ze9wG3" : {
"annotations" : {
"-KuWIRBARv7osWr3XDZz" : {
"annotationSubtitle" : "1 Cupertino CA",
"annotationTitle" : "Apple Infinite Loop",
"notes" : "Does it work?!",
}
How can I access the selected autoID so I can update the specific notes node. So far the best I have is:
guard let uid = Auth.auth().currentUser?.uid else { return }
uidRef.child(uid).child("annotations").(somehow access the specific childID).updateChildValues(["notes": newNotes])
Any help will be greatly appreciated. Thanks in advance
UPDATE
The annotationListItem struct is created:
struct AnnotationListItem {
let key: String?
var annotationTitle: String?
let annotationSubtitle: String?
let notes: String?
let ref: DatabaseReference?
init(key: String = "", annotationTitle: String, annotationSubtitle: String, notes: String) {
self.key = key
self.annotationTitle = annotationTitle
self.annotationSubtitle = annotationSubtitle
self.notes = notes
self.ref = nil
}
init(snapshot: DataSnapshot) {
key = snapshot.key
let snapshotValue = snapshot.value as! [String: AnyObject]
annotationTitle = snapshotValue["annotationTitle"] as? String
annotationSubtitle = snapshotValue["annotationSubtitle"] as? String
notes = snapshotValue["notes"] as? String
ref = snapshot.ref
}
init(Dictionary: [String: AnyObject]) {
self.key = Dictionary["key"] as? String
self.annotationTitle = Dictionary["annotationTitle"] as? String
self.annotationSubtitle = Dictionary["annotationSubtitle"] as? String
self.notes = Dictionary["notes"] as? String
self.ref = nil
}
func toAnyObject() -> Any {
return [
"annotationTitle": annotationTitle as Any,
"annotationSubtitle": annotationSubtitle as Any,
"notes": notes as Any
]
}
}
UPDATE
This is how the annotationListItem is created to be stored in Firebase:
// Using the current user’s data, create a new AnnotationListItem that is not completed by default
let uid = Auth.auth().currentUser?.uid
guard let email = Auth.auth().currentUser?.email else { return }
let title = placemark.name
let subtitle = annotation.subtitle
let notes = ""
// declare variables
let annotationListItem = AnnotationListItem(
annotationTitle: title!,
annotationSubtitle: subtitle!,
notes: notes)
// Add the annotation under their UID
let userAnnotationItemRef = uidRef.child(uid!).child("annotations").childByAutoId()
userAnnotationItemRef.setValue(annotationListItem.toAnyObject())
I think you only need to do this:(since you have declared the note as global)
guard let uid = Auth.auth().currentUser?.uid else { return }
uidRef.child(uid).child("annotations").(note.key).updateChildValues(["notes": newNotes])
inside the method where you change the notes
If I am not mistaken you are creating an array of a custom object?
var newAnnotations: [AnnotationListItem] = []
You could do something like: var newAnnotations: [(key: String, value: [String : Any])] = [] (Any only if you are going to have Strings, Integers, ect. If it'll only be String then specify it as a String.
Accessing the key would be: newAnnotations[indexPath.row].key in your cellForRowAtIndex of your tableView. Accessing values would be: newAnnotations[indexPath.row].value["NAME"].
You can have a separate array that holds the key and just append it at the same time as your population:
for item in snapshot.children {
guard let itemSnapshot = task as? FDataSnapshot else {
continue
}
let id = task.key //This is the ID
let annotationItem = AnnotationListItem(snapshot: item as! DataSnapshot)
newAnnotations.append(annotationItem)
}
Another thing you could do is go up one more level in your firebase call:
if let uid = Auth.auth().currentUser?.uid {
uidRef.child(uid).child("annotations").observeSingleEvent(of: .value, with: {snapshot in
if snapshot is NSNull{
//Handles error
} else{
if let value = snapshot.value as? NSDictionary{ //(or [String: String]
//set localDictionary equal to value
}
}
self.tableView.reloadSections([0], with: .fade)
})
}
And then when you select a row: let selectedItem = localDictionary.allKeys[indexPath.row] as! String //This is the ID you pass to your viewController.
I've spend hours looking at identical questions but none of the answers I've found are helping this issue. Simple app retrieves data from Firebase Database and passes to another view controller from the tableview. The main data will pass through but I can't edit the information without an identifying "key" which I tried to set as childByAutoID() but then changed to a timestamp. Regardless of the method, all I get is the entries info not the actual key itself.
func loadData() {
self.itemList.removeAll()
let ref = FIRDatabase.database().reference()
ref.child(userID!).child("MyStuff").observeSingleEvent(of: .value, with: { (snapshot) in
if let todoDict = snapshot.value as? [String:AnyObject] {
for (_,todoElement) in todoDict {
let todo = TheItems()
todo.itemName = todoElement["itemName"] as? String
todo.itemExpires = todoElement["itemExpires"] as? String
todo.itemType = todoElement["itemType"] as? String
self.itemList.append(todo)
print (snapshot.key);
}
}
self.tableView.reloadData()
}) { (error) in
print(error.localizedDescription)
}
}
If your data looks like this:
Uid: {
MyStuff: {
AutoID: {
itemName: “Apocalypse”,
itemExpires: “December 21, 2012”,
itemType: “Catastrophic”
}
}
}
Then I would query like this:
ref.child(userID!).child("MyStuff").observeSingleEvent(of: .value, with: { (snapshot) in
for child in snapshot.children {
let child = child as? DataSnapshot
let key = child?.key as? String
if let todoElement = child?.value as? [String: Any] {
let todo = TheItems()
todo.itemName = todoElement["itemName"] as? String
todo.itemExpires = todoElement["itemExpires"] as? String
todo.itemType = todoElement["itemType"] as? String
self.itemList.append(todo)
self.tableView.reloadData()
}
}
})
Additionally, like I said in my comment you can just upload the key with the data if you’re using .updateChildValues(). Example:
let key = ref.child("userID!").childByAutoId().key
let feed = ["key": key,
“itemName”: itemName] as [String: Any]
let post = ["\(key)" : feed]
ref.child("userID").child("MyStuff").updateChildValues(post) // might want a completionBlock
Then you can get the key the same way you are getting the rest of the values. So your new data would look like this:
Uid: {
MyStuff: {
AutoID: {
itemName: “Apocalypse”,
itemExpires: “December 21, 2012”,
itemType: “Catastrophic”,
key: “autoID”
}
}
}
The key you are trying to look for is located in the iterator of your for loop
Inside your if-let, try to do this:
for (key,todoElement) in todoDict {
print(key) // this is your childByAutoId key
}
This should solve the problem. Otherwise show us a screen of your database structure
I have an issue with my least favourite part in Firebase. I want to pull a post from user's following list (every user has one and only one post). First, I created a completion handler to get a list of all followers from Firebase and store it in userArray array of strings:
func GetUsersInFollowing(completion: #escaping (Bool) -> ()) {
ref.child("following").queryOrdered(byChild: FIRAuth.auth()!.currentUser!.uid).observeSingleEvent(of: .value, with: { (snapshot) in
for group in snapshot.children {
self.userArray.append((group as AnyObject).key)
}
completion(true)
})
}
Now the plan is to pull a post from every element of userArray.
Here is where the problem starts. I call CreatePosts() immediately after GetUsersInFollowing() completes.
func CreatePosts() {
for x in userArray {
var thePost = Post()
print("1")
self.ref.child("users").child(x).observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
thePost.fullName = value?["fullname"] as? String ?? ""
thePost.username = value?["username"] as? String ?? ""
thePost.profileImageURL = value?["photourl"] as? String ?? ""
print("2")
})
self.ref.child("posts").child(x).observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
thePost.description = value?["description"] as? String ?? ""
thePost.info = value?["location"] as? String ?? ""
thePost.postImageURL = value?["photoURL"] as? String ?? ""
thePost.timePost = value?["timestamp"] as? NSDate
thePost.upVotes = value?["upvotes"] as? Int ?? 0
})
self.postArray.append(thePost)
self.tableView.reloadData()
}
}
Everything looks ok to me, but it surely isn't. Here's how I create cells:
func configureCell(post: Post) {
self.post = post
self.username.text = post.username
self.profileImage = post.profileImageURL
print("3")
self.fullname.text = post.fullName
self.timestamp.text = post.timePost
self.upvotes.text = post.upVotes
self.location.text = post.location
self.descriptionText.text = post.description
}
The output in the console varies, but usually I get:
1
1
3
3
2
2
The idea is to first retrieve all data from Firebase, add it to post object, append the object to the array and then create cell for that object with data downloaded. Cell is already created even though data is not retrieved. I think that is the problem. Thank you, every suggestion is appreciated.
You need to inner query for combining both user profile data and post data.
Like this -
func CreatePosts() {
//Using userPostArrayObjFetched as a counter to check the number of data fetched.
//Remove this code, if you don't want to wait till all the user data is fetched.
var userPostArrayObjFetched = 0
for (index,userID) in userArray.enumerated() {
print("1" + userID)
self.ref.child("users").child(userID).observeSingleEvent(of: .value, with: { (snapshot) in
var thePost = Post()
let value = snapshot.value as? NSDictionary
thePost.fullName = value?["fullname"] as? String ?? ""
thePost.username = value?["username"] as? String ?? ""
thePost.profileImageURL = value?["photourl"] as? String ?? ""
print("2" + userID)
self.ref.child("posts").child(userID).observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
thePost.description = value?["description"] as? String ?? ""
thePost.info = value?["location"] as? String ?? ""
thePost.postImageURL = value?["photoURL"] as? String ?? ""
thePost.timePost = value?["timestamp"] as? NSDate
thePost.upVotes = value?["upvotes"] as? Int ?? 0
print("3" + userID)
self.postArray.append(thePost)
// Uncomment if you want to reload data as fetched from Firebase without waiting for all the data to be fetched.
// self.tableView.reloadData()
userPostArrayObjFetched += 1
if userPostArrayObjFetched == userArray.count{
self.tableView.reloadData()
}
})
})
}
}
So I am currently trying to take data from my Firebase database and set it as its own variable, but the child for each chart is a specific date and time (yy.mm.dd.h.m.s). So i have an array storing all the dates I need, but i cant reference them when calling my snapshot. I've tried these two methods which throw the error "(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''"
var postCollection = [170802120618, 170802101427] //yy.mm.dd.hh.mm.ss
ref.child("users").child(uid!).child("Posts").child(self.postCollection[indexPath.row]).observe(.value, with: { (snapshot) in
for item in snapshot.children{
let snapshotValue = snapshot.value as? NSDictionary
let firstNameSnap = snapshotValue?["First Name"] as? String ?? ""
currentCell.nameLabel.text = firstNameSnap
}
})
and
var postCollection = [170802120618, 170802101427] //yy.mm.dd.hh.mm.ss
let selection = self.postCollection[indexPath.row]
ref.child("users").child(uid!).child("Posts").child(self.postCollection[indexPath).observe(.value, with: { (snapshot) in
for item in snapshot.children{
let snapshotValue = snapshot.value as? NSDictionary
let firstNameSnap = snapshotValue?["First Name"] as? String ?? ""
currentCell.nameLabel.text = firstNameSnap
}
})
And the Database chart being roughly:
FIR{
users{
uid{
username: UserName
Posts{
170802120618{
First Name: first
}
}
}
}
}
Right. You want the child key to be an autogenerated hashvalue. You can create these by using childByAutoId(). Also if I were you, I would just store that dates as string and parse those as needed. Something below would be an example:
Posts {
-Kebfdajksthm {
first_name: "first",
post_date: "yymmddhhmmss"
}
}
Try This
var post = [String]()
ref.observe(.value, with: { (snapshot) in
for item in snapshot.children{
self.post.append((item as AnyObject).key)
}
})
Then you print "post" and you will get ["170802120618", "170802101427"]
func generateDataForRecents() {
if URLArrayStringThisSeason.count == 0 {
self.activityIndicator2.isHidden = false
self.activityIndicator2.startAnimating()
}
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("palettes").queryLimited(toFirst: 100).observe(.value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject]{
for each in snapDict as [String:AnyObject]{
let URL = each.value["URL"] as! String
self.URLArrayStringRecents.append(URL)
//print(self.URLArrayString.count)
//print(snapshot)
//let pictureTitle = each.value["title"] as! String
print(self.URLArrayStringRecents.count)
}
}
self.whatsNewCollectionView?.reloadData() //Reloads data after the number and all the URLs are fetched
self.activityIndicator2.stopAnimating()
self.activityIndicator2.isHidden = true
})
}
The following code does a retrieval of data each time the function is called, or when a new data is added.
This is extremely useful when the app is first started up or closed and then restarted. However, when the app is running, whenever a new entry is added, the code seemed to run again and thus appending twice the amount of new data.
For example, when there are already 15 entries identified and then suddenly a new entry is added, the array of the URL would contain 15+16 thus amounting to a total of 31.
How do I make it such that the new data is added to the array instead of adding the entire snapshot in?
You do that by listening for .childAdded events, instead of listening for .value:
var query = databaseRef.child("palettes").queryLimited(toFirst: 100)
query.observe(.childAdded, with: { (snapshot) in
let URL = snapshot.childSnapshot(forPath/: "URL").value as! String
self.URLArrayStringRecents.append(URL)
}
Since you have a limit-query, adding a 101st item means that one item will be removed from the view. So you'll want to handle .childRemoved too:
query.observe(.childRemoved, with: { (snapshot) in
// TODO: remove the item from snapshot.key from the araay
})
I recommend that you spend some time in the relevant documentation on handling child events before continuing.
Please check below method. I have use this method not getting any duplicate entry.
func getallNotes()
{
let firebaseNotesString: String = Firebase_notes.URL
let firebaseNotes = FIRDatabase.database().referenceFromURL(firebaseNotesString).child(email)
firebaseNotes.observeEventType(.Value, withBlock: { snapshot in
if snapshot.childSnapshotForPath("Category").hasChildren()
{
let child = snapshot.children
self.arrNotes = NSMutableArray()
self.arrDictKeys = NSMutableArray()
for itemsz in child
{
let childz = itemsz as! FIRDataSnapshot
let AcqChildKey : String = childz.key
if AcqChildKey == AcqIdGlobal
{
if (childz.hasChildren() == true)
{
let dictChild = childz.value as! NSMutableDictionary
self.arrDictKeys = NSMutableArray(array: dictChild.allKeys)
for i in 0..<self.arrDictKeys.count
{
let _key = self.arrDictKeys.objectAtIndex(i).description()
print(_key)
let dictData : NSMutableDictionary = NSMutableDictionary(dictionary: (dictChild.valueForKey(_key)?.mutableCopy())! as! [NSObject : AnyObject])
dictData.setObject(_key, forKey: "notesId")
self.arrNotes.addObject(dictData)
}
}
}
}
self.tableviewNote.reloadData()
}
})
}
As for the query for removed child,
query.observe(.childRemoved, with: { (snapshot) in
print(snapshot)
let URL = snapshot.childSnapshot(forPath: "URL").value as! String
self.URLArrayStringThisSeason = self.URLArrayStringThisSeason.filter() {$0 != URL}
self.thisSeasonCollectionView.reloadData()
})
it will obtain the URL of the removed child and then update the array accordingly.