Does anyone know how do you automatically update the child values to the tableView every time it changes? I have to terminate the app and when restarting it I'm able to to see the updated values. How can I update these without terminating the app each time?
var ref: DatabaseReference?
var databaseHandle: DatabaseHandle?
var postData = [String]()
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
databaseHandle = ref?.child("Posts").observe(.childAdded, with: { (snapshot) in
let post = (snapshot.value as AnyObject).description
if let actualPost = post {
postData.append(actualPost)
self.tableView.reloadData()
}
})
}
You should change
self.tableView.reloadData()
to
DispatchQueue.main.async {
self.tableView.reloadData()
}
that may work. Because all the UI-related works should happen in main queue
Related
I have tableView and refreshControl. I want that tableView's data reload only when I pull-to-refresh. I've made the function that reload data, but it works always.
Data reload after pull-to-refresh, but also observe update tableView.
How to remove it?
I want to make observe, reload data and remove observe (remove connection).
func reloadTable() {
let ref = Database.database().reference()
let userID = Auth.auth().currentUser?.uid
postData.removeAll()
var databaseHandle: DatabaseHandle?
databaseHandle = ref.child("doctors").child(userID!).observe(.childAdded) {
(snapshot) in
if let actualPost = snapshot.value as? String {
self.postData.append(actualPost)
self.table.reloadData()
}
}
// I tried this:
ref.removeAllObservers()
// And this as well:
ref.removeObserver(withHandle: databaseHandle)
}
You need
let current = ref.child("doctors").child(userID!)
current.observe ///
current.removeAllObservers()
if you need to removeObservers you need to go deep as you add childs , as removeAllObservers for parents doesn't remove them for childs
i have a viewcontroller with a tableview, and when user clicks on the cell, it goes to VC2. When the user has performed a action (and updated the values in VC2), i use self.dismiss(animated: true, completion: nil) to go back to the viewcontroller with the tableview, however the tableview (once the user has gone back to the tableview) is showing duplicated rows, but the child is succesfully deleted in firebase, and a new child is created - however the tableview is showing the childs that are not deleted twice.
This is all the relevant code in VC1:
class PostMessageListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var table: UITableView!
var topicID:namePosts?
let currentUserID = Auth.auth().currentUser?.uid
var posts = [Post]()
lazy var refresher: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.tintColor = .white
refreshControl.addTarget(self, action: #selector(requestData), for: .valueChanged)
return refreshControl
}()
#objc
func requestData() {
self.table.reloadData()
refresher.endRefreshing()
}
func reloadData(){
table.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
self.table.separatorStyle = UITableViewCellSeparatorStyle.none
table.refreshControl = refresher
//DataManager.shared.firstVC = self
self.table.delegate = self
self.table.dataSource = self
let postCell = UINib(nibName: "PostTableViewCell", bundle: nil)
self.table.register(postCell, forCellReuseIdentifier: "cell")
self.posts.removeAll()
Database.database().reference().child("posts").child(postID!.name)
.observe(.childAdded) { (snap) in
if snap.exists() {
//declare some values here...
self.posts.append( //some values here)
self.posts.sort(by: {$0.createdAt > $1.createdAt})
self.table.reloadData()
})
}
else {
self.table.reloadData()
}
}
//observe if a post is deleted by user
Database.database().reference().child("posts").child("posts").observe(.childRemoved) { (snapshot) in
let postToDelete = self.indexOfPosts(snapshot: snapshot)
self.posts.remove(at: postToDelete)
self.table.reloadData()
//self.table.deleteRows(at: [NSIndexPath(row: questionToDelete, section: 1) as IndexPath], with: UITableViewRowAnimation.automatic)
//self.posts.remove(at: indexPath.row)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.posts.count
}
func indexOfPosts(snapshot: DataSnapshot) -> Int {
var index = 0
for post in self.posts {
if (snapshot.key == post.postID) {
return index
}
index += 1
}
return -1
}
EDIT: Forgot to say, but i have used this code in another Viewcontroller, and it works fine there. However i just copied my code from that to this one, and deleted a bunch of stuff i didn't need, however i cant seem to find what i am missing in this one.
This may not be the answer but it may lead to an answer. As noted in the comments there are two arrays being used to manage the dataSource for the tableView. One contains the data and one is using an indexing technique - I believe that may lead to issues, like the one described in the question.
The other issue is that when every child is intially added, we re-sort the array and then refresh the tableView - that can lead to delays and flicker. (flicker = bad)
So let establish a couple of things. First a class that holds the posts
PostClass {
var post_id = ""
var post_text = ""
var creation_date = ""
}
second the Firebase structure, which is similar
posts
post_id_0
text: "the first post"
timestamp: "20190220"
post_id_1
text: "the second post"
timestamp: "20190221"
then a little trick to populate the datasource and leave a child added observer. This is important as you don't want to keep refreshing the tableView with every child it as may (will) flicker. So we leverage that childAdded events always come before .value events so the array will populate, and then .value will refresh it once, and then we will update the tableView each time after. Here's some code - there's a lot going on so step through it.
var postsArray = [String]()
var initialLoad = true
func ReadPosts() {
let postsRef = self.ref.child("posts").queryOrdered(byChild: "timestamp")
postsRef.observe(.childAdded, with: { snapshot in
let aPost = PostClass()
aPost.post_id = snapshot.key
aPost.post_text = snapshot.childSnapshot("text").value as! String
aPost.creation_date = snapshot.childSnapshot("timestamp").value as! String
self.postsArray.append(aPost)
//upon first load, don't reload the tableView until all children are loaded
if ( self.initialLoad == false ) {
self.postsTableView.reloadData()
}
})
//when a child is removed, the event will contain that child snapshot
// we locate the child node via it's key within the array and remove it
// then reload the tableView
postsRef.observe(.childRemoved, with: { snapshot in
let keyToRemove = snapshot.key
let i = self.postsArray.index(where: { $0.post_id == keyToRemove})
self.postsArray.remove(at: i)
self.postsTableView.reloadData()
})
//this event will fire *after* all of the child nodes were loaded
// in the .childAdded observer. So children are sorted, added and then
// the tableView is refreshed. Set initialLoad to false so the next childAdded
// after the initial load will refresh accordingly.
postsRef.observeSingleEvent(of: .value, with: { snapshot in
self.postsTableView.reloadData()
self.initialLoad = false
})
}
Things to note
We are letting Firebase doing the heavy lifting and ordering the nodes by creation_date so they come in order.
This would be called from say, viewDidLoad, where we would set the initialLoad class var to true initially
I am new to programming and currently building an app where I collect data from Firebase.
I thought it would be nice to implement this refreshing animation, when the view is pulled down. Now I have the problem that nothing gets refreshed at all (it was working fine in the beginning). Also every time the app launches the tableView is empty at first and when you make the pushdown it reloads, but wrong.
I am really struggling. Can anyone see anything wrong in my code?
class ViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var newsfeedTableView: UITableView!
#IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
private let refreshControl = UIRefreshControl()
var ref: DatabaseReference!
var posts = [String]()
var timeRef = [String]()
var userRef = [String]()
override func viewDidLoad() {
super.viewDidLoad()
addSlideMenuButton()
setupTableView()
ref = Database.database().reference()
ref.child("posts").observe( .childAdded, with: { snapshot in
let newPost = snapshot.value as? NSDictionary
let post_title:String = newPost!["post_title"] as? String ?? "error"
let newPostTime = snapshot.value as? NSDictionary
let post_time:String = newPostTime!["post_time"] as? String ?? "error collecting timestamp"
let newPostUser = snapshot.value as? NSDictionary
let post_user:String = newPostUser!["post_user"] as? String ?? "Team"
self.posts.insert(post_title, at: 0)
self.timeRef.insert(post_time, at: 0)
self.userRef.insert(post_user, at: 0)
self.newsfeedTableView.reloadData()
})
}
private func setupTableView() {
if #available(iOS 10.0, *) {
newsfeedTableView.refreshControl = refreshControl
} else {
newsfeedTableView.addSubview(refreshControl)
}
refreshControl.addTarget(self, action: #selector(refreshNewsFeedTableView(_:)), for: .valueChanged)
}
#objc private func refreshNewsFeedTableView(_ sender: Any) {
//self.refreshControl.beginRefreshing()
self.newsfeedTableView.reloadData()
self.refreshControl.endRefreshing()
self.activityIndicatorView.stopAnimating()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NewsfeedCell
cell.customTextLabel?.text = posts[indexPath.row]
cell.customTimeLabel?.text = timeRef[indexPath.row]
cell.customNameLabel?.text = userRef[indexPath.row]
return cell
}
}
class NewsfeedCell: UITableViewCell {
#IBOutlet weak var customTextLabel: UILabel!
#IBOutlet weak var customNameLabel: UILabel!
#IBOutlet weak var customTimeLabel: UILabel!
}
Here
#objc private func refreshNewsFeedTableView(_ sender: Any)
you don't do any API requests to get the data then reload the table and end refreshing , and since you use observe here
ref.child("posts").observe( .childAdded, with: { snapshot in
then you'll get any new data , if you need to implement the refresh then insert all database code inside a function and replace observe with observeSingleEvent and call it from viewDidLoad and from the refresh selector method
ref.child("posts").observeSingleEvent( .value, with: { snapshot in
Note with the suggested approach you will get all the childs at a time so don't forget to clean the array inside the callback of observeSingleEvent to avoid duplicating values , also consider making one dataSource array instead of 3 here
self.posts.insert(post_title, at: 0)
self.timeRef.insert(post_time, at: 0)
self.userRef.insert(post_user, at: 0)
probably with creating a model
struct Item {
let post:String
let time:String
let user:String
}
Only problem I can see in the code is, you are using .childAdded to observe events from firebase but according to firebase document:
The child_added event is typically used when retrieving a list of
items from the database. Unlike value which returns the entire
contents of the location, child_added is triggered once for each
existing child and then again every time a new child is added to the
specified path. The event callback is passed a snapshot containing the
new child's data. For ordering purposes, it is also passed a second
argument containing the key of the previous child.
So, if you want to receive all the data of any particular node, you need to use value event but not child_added as stated here:
The value event is used to read a static snapshot of the contents
at a given database path, as they existed at the time of the read
event. It is triggered once with the initial data and again every time
the data changes. The event callback is passed a snapshot containing
all data at that location, including child data. In the code example
above, value returned all of the blog posts in your app. Everytime a
new blog post is added, the callback function will return all of the
posts.
Reload UItableView in DispatchQueue.main, try this code.
override func viewDidLoad() {
super.viewDidLoad()
addSlideMenuButton()
setupTableView()
ref = Database.database().reference()
ref.child("posts").observe( .childAdded, with: { snapshot in
let newPost = snapshot.value as? NSDictionary
let post_title:String = newPost!["post_title"] as? String ?? "error"
let newPostTime = snapshot.value as? NSDictionary
let post_time:String = newPostTime!["post_time"] as? String ?? "error collecting timestamp"
let newPostUser = snapshot.value as? NSDictionary
let post_user:String = newPostUser!["post_user"] as? String ?? "Team"
self.posts.insert(post_title, at: 0)
self.timeRef.insert(post_time, at: 0)
self.userRef.insert(post_user, at: 0)
DispatchQueue.main.async() {
self.newsfeedTableView.reloadData()
}
})
}
I'm trying to get certain child nodes named City from Firebase using observeSingleEvent but I am having issues trying to pull it into the main thread. I have used a combination of completion handlers and dispatch calls but I am not sure what I am doing wrong, in addition to not being that great in async stuff. In viewDidLoad I'm trying to append my keys from the setupSavedLocations function and return it back to savedLocations I feel like I am close. What am I missing?
Edit: Clarity on question
import UIKit
import Firebase
class SavedLocationsViewController: UIViewController {
var userID: String?
var savedLocations: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
self.savedLocations = savedData
print("inside", self.savedLocations)
})
}
print("outside",savedLocations)
}
func setupSavedLocations(completion: #escaping ([String]) -> ()) {
guard let user = userID else { return }
let databaseRef = Database.database().reference(fromURL: "https://************/City")
var dataTest : [String] = []
databaseRef.observeSingleEvent(of: .value, with: {(snapshot) in
let childString = "Users/" + user + "/City"
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
dataTest.append(key)
}
completion(dataTest)
})
}
sample output
outside []
inside ["New York City", "San Francisco"]
The call to setupSavedLocations is asynchronous and takes longer to run than it does for the cpu to finish viewDidLoad that is why your data is not being shown. You can also notice from your output that outside is called before inside demonstrating that. The proper way to handle this scenario is to show the user that they need to wait for an IO call to be made and then show them the relevant information when you have it like below.
class SavedLocationsViewController: UIViewController {
var myActivityIndicator: UIActivityIndicatorView?
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
showSavedLocations(locations: savedData)
})
}
// We don't have any data here yet from the IO call
// so we show the user an indicator that the call is
// being made and they have to wait
let myActivityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)
myActivityIndicator.center = view.center
myActivityIndicator.startAnimating()
self.view.addSubview(myActivityIndicator)
self.myActivityIndicator = myActivityIndicator
}
func showSavedLocations(locations: [String]) {
// This function has now been called and the data is passed in.
// Indicate to the user that the loading has finished by
// removing the activity indicator
myActivityIndicator?.stopAnimating()
myActivityIndicator?.removeFromSuperview()
// Now that we have the data you can do whatever you want with it here
print("Show updated locations: \(locations)")
}
I am using Firebase as my data structure. I use completion handler in my UITableView's refreshControl, in order to stop refreshing when finish loading all data from Firebase.
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl = UIRefreshControl()
self.refreshControl!.addTarget(self, action: #selector(refreshData),for: .valueChanged)
self.refreshControl!.attributedTitle = NSAttributedString(string: "Update the data")
refreshData{ _ in
self.refreshControl!.endRefreshing()
}
}
And this is my refreshData method
func refreshData(completionHandler:#escaping (Bool)->() ) {
//Remove old data
self.items.removeAll()
//Renew all data
var ref: DatabaseReference!
ref = Database.database().reference(withPath: "tasks")
//Loading local drafts
var drafts : [Task]!
if let local_drafts = NSKeyedUnarchiver.unarchiveObject(withFile: Task.ArchiveURL.path) as? [Task] {
drafts = local_drafts
}
else{
drafts = []
}
//Reloading the database
ref.observe(.value, with: { snapshot in
var newItems: [Task] = []
self.num_of_tasks = Int(snapshot.childrenCount)
for item in snapshot.children {
//let local = item as! DataSnapshot
//let snapshotValue = local.value as! [String: AnyObject]
//print(snapshotValue["main_content"] as! String!)
let taskItem = Task(snapshot: item as! DataSnapshot)
newItems.append(taskItem!)
}
let merged = drafts + newItems
self.items = merged
completionHandler(true) //THIS LINE HAS ERR_BAD_ACCESS
})
}
I think the problem might be the two refreshData in viewDidLoad. But I don't know how to fix it. How could I add refreshData with handler as a selector?
This is not a thread problem. I solve it by wrapping the function call in another function because in this line
self.refreshControl!.addTarget(self, action: #selector(refreshData),for: .valueChanged)
I tried to use refreshData as a selector, but actually, selector doesn't have any completion handler by itself, so that caused the memory error whenever the user tried to renew by drag the scene down. So by wrapping the function call in another function
func refresh(){
refreshData{ [weak self] _ in
if let _ = self {
self?.refreshControl!.endRefreshing()
}
}
}
and use this function in the selector, it will deliver the right completion handler, and thus solve the problem.