Updating a UITableView on Firebase Data Change - ios

So I am trying to have table cells that auto refresh when any other of the cells are changed. I hoped that my code could find the object within the table array, change a value, and refresh the table view all in one fell swoop, but for some reason, even though my array updates, the table refuses to instantly update. To see my new values I have to restart the app or reopen the View.
This is what I am doing so far:
usersRef.queryOrdered(byChild "contactList/(currentUID!)").queryEqual(toValue: true).observe(.childChanged, with: { (snapshot) in
if let friends = snapshot.value as? [String: AnyObject] {
//Making a new updated contact
let name = friends["name"] as! String
print(name)
let edit = Contact()
edit.name = name;
edit.isSafe = friends["isSafe"] as! Bool
edit.email = friends["email"] as! String
//My manager object has a custom array objects called contacts.
self.manager.index(withName: name, contact: edit)
self.tableView.reloadData();
} else {
print("Failed to Convert")
}
})
}
index(withName: String, contact: Contact) method in the ContactManager:
//This method goes through the contactList to check names to see which contact has a specific name equal to "name", then it uses the "contact" to update that specific child.
public mutating func index(withName: String, contact: Contact) {
for iterator in 0 ..< contactList.count {
if (contactList[iterator].name == withName) {
contactList[iterator] = contact
}
}
}

This is a common problem. Your calling self.tableView.reloadData() in the background thread. All UI changes need to be made on the main thread. like this.
DispatchQueue.main.async {
self.tableView.reloadData();
}

Related

Cache server data globally and refresh views

I'm building an app that uses firebase for authentication and database functionality. Once a user signs up, a database record is stored for that user, containing some basic information like first name, last name etc.
Once a user logs in with his credentials I want to set a global variable (perhaps userDefaults?) which contains the user data for that specific user. Otherwise I have to fetch user data for every time I want to fill a label with for instance, a user's first name.
I managed to set userdefaults upon login and use this info in UIlables. But when I let users make changes to their data, of which some is important for the functioning of the app, I can update the server AND the userdefaults but the app itself doesn't update with the correct data. It keeps the old data in (for example) UIlables.
I would love to get some more insight on what the best work-flow is to manage situations like these.
When opening the app, i have a tabBarController set as rootviewcontroller. In the load of tabbarcontroller I have the following code retrieving the user data from firebase and saving it to userdefaults:
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot.value ?? "")
guard let dictionary = snapshot.value as? [String: Any] else { return }
let firstname = dictionary["First name"] as? String
let lastname = dictionary["Last name"] as? String
print("first name is: " + firstname!)
UserDefaults.standard.set(firstname, forKey: "userFirstName")
print(UserDefaults.standard.value(forKey: "userFirstName"))
self.setupViewControllers()
}
Then I continue on loading in all the viewcontrollers in the tabBarController:
self.setupViewControllers()
During that process the labels in those viewcontrollers get filled in with the userdefaults data.
This is an example of a label being filled in with userDefaults but not being updated upon changing of userdefaults:
let welcomeLabel: UILabel = {
let label = UILabel()
let attributedText = NSMutableAttributedString(string: "Welcome ")
attributedText.append(NSAttributedString(string: "\(UserDefaults.standard.string(forKey: "userFirstName")!)"))
label.attributedText = attributedText
label.font = UIFont.systemFont(ofSize: 30, weight: .bold)
return label
}()
this is a function i'm using to update the first name (via a textfield filled in by the user):
#objc func updateName() {
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(uid).updateChildValues(["First name" : updateNameField.text ?? ""])
UserDefaults.standard.set(updateNameField.text, forKey: "userFirstName")
print(UserDefaults.standard.value(forKey: "userFirstName"))
}
So you'll have to organize things first. In a new file define constants such as below. These constant will be accessible in global scope unless private
Constants.swift
private let storedusername = "usname"
private let storedName = "uname"
private let displaypic = "udp"
private let aboutme = "udesc"
var myusername : String {
get {
return (UserDefaults.standard.string(forKey: storedusername)!)
} set {
UserDefaults.standard.set(newValue, forKey: storedusername)
}
}
var myname : String {
get {
return (UserDefaults.standard.string(forKey: storedName)!)
} set {
UserDefaults.standard.set(newValue, forKey: storedName)
}
}
var myProfileImage : Data {
get {
return (UserDefaults.standard.data(forKey: displaypic)!)
} set {
UserDefaults.standard.set(newValue, forKey: displaypic)
}
}
var myAboutMe : String? {
get {
return (UserDefaults.standard.string(forKey: aboutme)!)
} set {
UserDefaults.standard.set(newValue, forKey: aboutme)
}
}
Now the next time you want to save anything in UserDefaults, you'll just do the following anywhere throughout your code base :
myusername = "#CVEIjk"
And to retrive it, just call it :
print(myusername)
IMPORTANT NOTE --
Always remember to initialize them. You can do this as the user signs up. As soon as they fill out their details and hit submit, just save them to these variables. That wouldn't cause unnecessary crash.
You'll have to save them at every location you perform updates regarding these nodes in the database.
Now, the refreshing views part. I am taking a scenario where your ProfileView.swift has the view and user goes to EditProfile.swift for updating the content.
You initialize all your observers the place where the update will have the immediate effect. Because the view immediately after the update matters. The rest will be called through the getter of the aboutme
ProfileView.swift
func openEditView() {
NotificationCenter.default.addObserver(self, selector: #selector(fetchUserDetails), name: Notification.Name("update"), object: nil)
//setting this right before the segue will create an observer specifically and exclusively for updates. Hence you don't have to worry about the extra observers.
perform(segue: With Identifier:)// Goes to the editProfile page
}
This function will be initially called in viewDidLoad(). At this time you need to make sure you have all the data, else it will produce no values. But if you are storing everything as the user signs up, you are safe.
#objc func fetchUserDetails() {
if uid != nil {
if myname.count > 0 { // This will check if the variable has anything in the memory or not. Dont confuse this with [Array].count
self.nameLabel = myname
}
}
}
This function also acts an ab observer method. So when the notifications are posted they can run again.
Now, EditProfile.swift
In the block where you are updating the server, save the values and then create a Notification.post and put this method right before you dismiss(toViewController:)
func updateUserCacheData(name: String, username: String, aboutme: String, ProfilePhoto: UIImage? = nil) {
DispatchQueue.global().async {
myname = name
myusername = username
myAboutMe = aboutme
if self.newImage != nil {
myProfileImage = self.newImage!.jpegData(compressionQuality: 1)!
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .refreshProfileViews, object: nil)
}
}
}
func updateToServerAndBackToProfileView() {
self.updateUserCacheData(name: iname!, username: iusername, aboutme: iaboutme!)
self.dismiss(animated: true, completion: nil)
}
}
As long as this goes back to ProfileView, your views will be instantly refreshed. You can keep an observer wherever you view will be first displayed after the dismiss. the rest will fetch updated content always. Also, don't forget to deinit your Observer in ProfileView
//This code is in ProfileView.swift
deinit {
NotificationCenter.default.removeObserver(self, name: Notification.Name("update"), object: nil)
}
Also, in cases where the content might be empty, simply initialize it with empty content. For example, if user doesn't choose to add aboutme while signing up, you can just put
`myaboutme = ""`
This will create a safe environment for you and you are well set.

I need to update a TableView every so often but doing so duplicates the cells

I have a TableView that I am updating from time to time with a Timer so that the data of the TableView changes if necessary... What happens is that the data is updated but it is duplicated and it does not eliminate the data that it had previously, then it is generating a giant TableView.
How could I make them update but delete the data I had previously and leave only the new cells when the tableview is updated?
This is the code that I use in the timer to update the TV:
#objc func updatetableview(){
databaseRef.child("A_Usuarios").queryOrdered(byChild: "TipoUsuario").queryEqual(toValue: "Empresa").observe(.childAdded, with: { (snapshot) in
let key = snapshot.key
self.snap = (snapshot.value as? NSDictionary)!
self.snap.setValue(key, forKey: "Uid")
self.city = self.snap["Ciudad"] as? String ?? ""
self.activo = self.snap["Activo"] as? String ?? ""
if self.city == self.cdad && self.activo != "No" {
if(key == self.loggedInUser?.uid){
print("Same as logged in user, so don't show!")
}
else
{
self.usersArray.append(self.snap)
//insert the rows
self.tableview.insertRows(at: [IndexPath(row:self.usersArray.count-1,section:0)], with: UITableView.RowAnimation.automatic)
}
}
}) { (error) in
print(error.localizedDescription)
}
I hope you can help me, thank you very much!
You need to clear the array before doing another observe
#objc func updatetableview(){
usersArray.removeAll()
....
}
BTW .observe(.childAdded is supposed to do the job , so you may need n't to do this
The observer you have used returns all the values in the given path. This closure is called when a new child is added. But it gets all the available in that path, not only the newly added data.
This is why it is generating a giant TableView.
So you need to get the newly added data only using queryLimited(toLast: UInt)
databaseRef.child("A_Usuarios").queryOrdered(byChild: "TipoUsuario").queryEqual(toValue: "Empresa").queryLimited(toLast: 1).observe(.childAdded, with: { (snapshot) in
In firebase, if you are observing same node 5 times then it will give you 5 events. So you are adding same record 5 times. So you need to check that you are already observing that node before observing. Following code creating a problem. Every time you call updateTableView. It is adding new observer and that why you get same records multiple times.
databaseRef.child("A_Usuarios").queryOrdered(byChild: "TipoUsuario").queryEqual(toValue: "Empresa").observe(.childAdded, with: { (snapshot) in

How can I stop UICollectionView from showing duplicate items after Firebase update

I have two UICollection views on a page that displays data about a Room. It includes photos of the room in one UICollection View and another UICollection View which contains a list of items in that room. There's a link to edit the Room. When a user clicks on the link, they then segue to another view that let's them update it including adding additional photos.
After adding a photo, and hitting submit, in the background the photo is uploaded to Firebase storage and in the Firebase database, the record is updated to include the name of the file that was just uploaded. Meanwhile, the user is segued back to the Room view.
There's a watched on the record of the room in Firebase and when it updates, then the view is refreshed with new data. This is where the problem occurs. It appears, based on a lot of debugging that I've been doing, that the Observe method fires twice and what ends up happening, is the UICollection view that holds the images of the room will show duplicates of the last photo added.
For example, if I add one photo to the room, that photo will appear in the collection 2x. I've attempted to clear the array before the array is updated with the images, and from my analysis, it appears that the array only contains two items, despite showing three in the view. I'm not sure what is happening that would cause this?
Here's a link to the entire file, because I think it might help.
Here's the loadData() method in case this is all that's important:
func loadData() {
self.ref = Database.database().reference()
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
guard let userID = Auth.auth().currentUser?.uid else { return }
let buildingRef = self.ref.child("buildings").child(userID)
buildingRef.keepSynced(true)
buildingRef.child(self.selected_building as String).observe(DataEventType.value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
if ((value) != nil) {
let building_id = value?["id"] as! String
let saved_image = value?["imageName"] as! String
let user_id = userID as! String
let destination = "/images/buildings/\(userID)/\(building_id)/"
let slideShowDictionary = value?["images"] as? NSDictionary
if ((slideShowDictionary) != nil) {
self.slideShowImages = [UIImage]()
self.slideShowCollection.reloadData()
var last_value = ""
slideShowDictionary?.forEach({ (_,value) in
print("are they different? \(last_value != (value as! String))")
if (last_value != value as! String) {
print("count: \(self.slideShowImages.count)")
print("last_value \(last_value)")
print("value \(value)")
last_value = value as! String
CloudStorage.instance.downloadImage(reference: destination, image_key: value as! String, completion: { (image) in
self.slideShowImages.append(image)
self.slideShowCollection.reloadData()
})
}
})
CloudData.instance.getBuildingById(userId: user_id, buildingId: building_id, completion: { (building) in
self.title = building.buildingName as String
self.roomsCollection.reloadData()
})
}
}
})
// User is signed in.
self.getRooms()
}
I am not completely familiar with the Firebase API but if you are having issues with the observation I would suspect the following:
#IBAction func unwindToRoomsVC(segue:UIStoryboardSegue) {
loadData()
}
Triggering loadData a second time looks like it would add a second observation block. As best I can tell the .observe method probably persists the block it is given and triggers it on all changes.

How to delay a return call in Swift

Hello guys I am currently working on a program that holds a list of books in a UITableView. As you know, the TableView takes two methods, one with cellForRowAtIndexPath and the one I will be talking about today, numberOfRowsInSection. So the problem I am having is that I access my database to get the number of books that are currently in the database in order to return how many indices I will need in the array of Book stucts. So I have two groups, buy and sell, that may or may not have any books in them.
Anyway, I populate my array (it's empty to start with) and then I return the books.count as the numberOfRowsInSection. The problem is that I am consistently returning 0 seeing as the array gets populated after the return is executed.
Below is my code.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
populateArray()
print("books.count: ",books.count)
return books.count // KEEPS RETURNING 0 BC IT HASN'T POPULATED YET *ARRRRRRGH*
}
func populateArray(){
print("started looking")
var indices = 0
if divider.selectedSegmentIndex == 0{
ref.child(school).observeEventType(.Value, withBlock: { (snapshot) in
let numSelling = snapshot.value!["numSelling"] as! Int // gets numSelling
if numSelling > 0 {
self.noEntries = false
print("numSelling: ", numSelling) //see console
indices = numSelling
}else{
self.noEntries = true
indices = 1
print("No Values Selling")
}
}) { (error) in
print(error.localizedDescription)
}
}else{
ref.child(school).observeEventType(.Value, withBlock: { (snapshot) in
let numBuying = snapshot.value!["numBuying"] as! Int // gets numBuying
if numBuying > 0 {
self.noEntries = false
print("numBuying: ", numBuying) //see console
indices = numBuying
}else{
self.noEntries = true
indices = 1
}
}) { (error) in
print(error.localizedDescription)
}
}
delay(0.5){
print("ind: ", indices) // printing correctly
if(self.noEntries){ // just add one book to get the indices to be 1
self.books.append(Book(isbn: "", title: "", author: "", edition: "", price: "", uid: ""))
return
}
if self.divider.selectedSegmentIndex == 0{
self.ref.child(self.school).child("selling").observeEventType(.Value, withBlock: { (snapshot) in
let booksJSON = snapshot.value! as! NSArray
for bookJSON in booksJSON { // add the book to the array
let tempAuthor = bookJSON["authors"] as! String
let tempTitle = bookJSON["title"] as! String
let tempEdition = bookJSON["edition"] as! String
let tempPrice = bookJSON["price"] as! String
let tempISBN = bookJSON["isbn"] as! String
let tempUID = bookJSON["uid"] as! String
self.books.append(Book(isbn: tempISBN, title: tempTitle, author: tempAuthor, edition: tempEdition, price: tempPrice, uid: tempUID))
}
}) { (error) in
print(error.localizedDescription)
}
}else if self.divider.selectedSegmentIndex == 1{
self.ref.child(self.school).child("buying").observeEventType(.Value, withBlock: { (snapshot) in
let booksJSON = snapshot.value! as! NSArray
for bookJSON in booksJSON { // add the book to the array
let tempAuthor = bookJSON["authors"] as! String
let tempTitle = bookJSON["title"] as! String
let tempEdition = bookJSON["edition"] as! String
let tempPrice = bookJSON["price"] as! String
let tempISBN = bookJSON["isbn"] as! String
let tempUID = bookJSON["uid"] as! String
self.books.append(Book(isbn: tempISBN, title: tempTitle, author: tempAuthor, edition: tempEdition, price: tempPrice, uid: tempUID))
}
}) { (error) in
print(error.localizedDescription)
}
}
}
}
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
Keep in mind that I cannot make a callback in this method because it is automatically called by the program when the view is loaded.
Also, the delay segments are in efforts to stop the same thing from happening. Problem is that I cannot put the delay around the return because it thinks I want to return an Int for the delay block.
Console:
started looking
books.count: 0
started looking
books.count: 0
started looking
books.count: 0
started looking
books.count: 0
numSelling: 6
numSelling: 6
numSelling: 6
numSelling: 6
ind: 6
ind: 6
ind: 6
ind: 6
As you can see it is returning 0 before it even gets to the numSelling value from the database.
Thank you so much for your help and have a great day!
You cannot delay returning to a method once it has been called, but you can ask the table view to call the data source methods again.
The easiest solution would be to call reloadData() on your table view once your data has been populated (i.e., at the end of your populateArray() method). I would probably also move the call to populateArray() somewhere else (perhaps viewDidLoad(), if that's appropriate).
I would:
Move the "retrieve titles being bought/sold" out of the delay method. Call that from within the respective observeEventType of numSelling/numBuying. Get rid of the delay.
As Charles says, don't worry that the values are empty when viewDidLoad finishes. Just have the routine call tableView.reloadData() when it's done.
Assuming your UI is showing both titles being bought and sold at the same time (or that you're jumping between them quickly and don't want to wait for the data to be retrieved), your routine might want to go ahead and retrieve both arrays and only call reloadData when both are done. This means that you might need two arrays in your model, one for booksSelling and one for booksBuying.

How can I prevent TableView cells from duplicating? Swift

So I am building a notes app and have tried everything but can not figure this issue out. I have 3 UIViewController's. When you click a button on the first UIViewController, it shoots you over to the third one which consist of a UITextView, UITextField and a Static UILabel which gets updated with some information. When you fill out these fields and tap the back button it brings you to the second view controller which is a table that gets updated with this information.
The issue is: when I tap the UITableViewCell it loads the information back to the third view controller so the user can edit his notes but when I come back to the UITableView it creates a brand new cell instead of just updating the old one.
If I could just update my array with the same object I sent back to be edited by the user I think this issue would be solved but I have no idea how to do this. Thanks for the help!
VC2 - this is how I am sending my data from the tableView to the textView Back
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let nextView = segue.destinationViewController as! TextViewController
guard let indexPath = self.tableView.indexPathForSelectedRow else {
print("Didnt work")
return
}
let dataToSendBackToBeEdited = textViewObjectData[indexPath.row]
print(dataToSendBackToBeEdited.dreamText)
print(dataToSendBackToBeEdited.dreamTitle)
print(dataToSendBackToBeEdited.dreamType)
print(dataToSendBackToBeEdited.description)
print(dataToSendBackToBeEdited)
nextView.objectSentFromTableViewCell = dataToSendBackToBeEdited
}
This is how I am saving the information the the user taps back to go to the tableView
func determineSave() {
guard var savedDreamArray = NSKeyedUnarchiver.unarchiveObjectWithFile(TextViewController.pathToArchieve.ArchiveURL.path!) as? [Dream] else {
//First dream object saved
let dreamObject = Dream(dreamTitle: titleForDream.text!, dreamType: typeOfDreamLabel.text!, dreamText: textView.text, currentDate: NSDate())
dreamArray.append(dreamObject)
NSKeyedArchiver.archiveRootObject(dreamArray, toFile: pathToArchieve.ArchiveURL.path!)
return
}
//print(savedDreamArray.count)
//print(savedDreamArray.first!.dreamTitle)
let dreamObject = Dream(dreamTitle: titleForDream.text!, dreamType: typeOfDreamLabel.text!, dreamText: textView.text!, currentDate: NSDate())
savedDreamArray.append(dreamObject)
NSKeyedArchiver.archiveRootObject(savedDreamArray, toFile: pathToArchieve.ArchiveURL.path!)
}
I was having this issue as well. Came in here, and got the answer. The array!
I was appending the array as well, which apparently was causing duplicate cells to appear.
I just reset my array back to empty before I retrieved the data again and reloaded the table.
I'm using Firebase so my code looks like this:
DataService.instance.dateProfileRef.observeEventType(FIRDataEventType.Value) { (snapshot: FIRDataSnapshot)
in
//have to clear the array here first before reloading the data. otherwise you get duplicates
self.posts = [] //added this line
if let snapshot = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshot {
let snapUserID = (snap.childSnapshotForPath("userID").value!)
if snapUserID as? String == USER_ID {
if let profileDict = snap.value as? Dictionary<String, AnyObject> {
let key = snap.key
let post = Post(postKey: key, postData: profileDict)
self.posts.append(post)
You do a .append() on your array, this will add a new cell.
You must find the index of the object you want to update and then assign it:
array[index] = newObject

Resources