Swift: Can't synchronize data to table view using Firebase - ios

I was working on a grocery list app. When I tried to synchronise data to table view using Firebase, the grocery item can not be shown on the table view. However, when I reset the simulator, items added before can be shown on the table view, but when I add another item, it can't be shown on the table view. I double checked the code and found nothing wrong. Could you help me fix it?(e.g. In the following two images, the coffee item can not be shown)
Part of code in GroceryListViewController.swift
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
//Attach a listener to receive updates.
ref.observeSingleEventOfType(.Value, withBlock: { (snapshot) in
var newItems = [GroceryItem]()
for item in snapshot.children
{
let groceryItem = GroceryItem(snapshot: item as! FDataSnapshot)
newItems.append(groceryItem)
}
self.items = newItems
self.tableView.reloadData()
})
}

1 - You are using observeSingleEventOfType (* This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.) From the doc. So your listener is called once and stop listening. Use observeEventType instead
2 - You should do the reloadData of your tableview on the main thread like this :
NSOperationQueue.mainQueue().addOperationWithBlock({
self.tableView.reloadData()
})
The reason is that you cannot / must not interact with UI from a background thread. Since Firebase is asynchronous the completion block is on a background thread

Maybe it's obvious, but it happen to me. I was debugging and the code was stopped in a breakpoint, and data was not in the console in firebase. Then I realized that I had to let it run completely to refresh. Maybe it can help...

Add this code once touch the button "+":
self.items.append(groceryItem)
let row = self.items.count - 1
let indexPath = NSIndexPath(forRow: row, inSection: 0)
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
This ensures the interaction with UI.

Related

Delete multiple cells from tableView without conflict

I have a tableView where the user can tap on a button inside the cell to delete it. That button is connected with this delegate-function:
extension WishlistViewController: DeleteWishDelegate {
func deleteWish(_ idx: IndexPath){
// remove the wish from the user's currently selected wishlist
wishList.wishes.remove(at: idx.row)
// set the updated data as the data for the table view
theTableView.wishData.remove(at: idx.row)
self.theTableView.tableView.deleteRows(at: [idx], with: .right)
print("deleted")
}
}
Here is how I call the callback (after an animation is finished):
#objc func checkButtonTapped(){
self.successAnimation.isHidden = false
self.successAnimation.play { (completion) in
if completion {
self.deleteWishCallback?()
}
}
}
And this callback is handled in cellForRowAt and passes the indexPath:
cell.deleteWishCallback = {
self.deleteWishDelegate?.deleteWish(indexPath)
}
It works fine until the user clicks multiple buttons right after another as I get a IndexOutOfBounds-Error. What I was thinking of is to store all the incoming indexes in some sort of list and delete them one after another but each index changes as soon as another cell below itself is deleted. What is the best way to get this done?
How are you sending the indexPath to delete in the delegate, can you show code?
You might have retain cycle on your deleted cell, and the index is not valid.
Edit: Solution
You must pass the cell in your closure self.deleteWishCallback?(cell) and then get the actual index path like this
cell.deleteWishCallback = { deletedCell in
let indexPath = tableView.indexPath(for: deletedCell)
wishList.wishes.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .right)
self.deleteWishDelegate?.deleteWish(indexPath)
}
Instead of self.theTableView.tableView.deleteRows(at: [idx], with: .right) just simply reloadData. (self.theTableView.reloadData())
If you set the new datasource for your tableView, you should reload cells.

Charts lib lags on scroll

I've just implemented the library Charts (https://github.com/danielgindi/Charts) in a tableview, but I experience pretty heavy lag during scrolling, when my charts hold a lot of data.
I have a method inside the ChartTableViewCell where I draw the chart based on the data I pass and call from my viewcontroller.
func updateCell(chartData: ChartData) {
DispatchQueue.global(qos: .background).async {
print("This is run on the background queue")
self.readings = (readings != nil) ? readings!.readings : []
self.period = period
if (chartData.isKind(of: LineChartData.self)) {
data.lineData = chartData as! LineChartData
}
else if (chartData.isKind(of: BarChartData.self)) {
data.barData = chartData as! BarChartData
}
}
DispatchQueue.main.async {
self.chartView.data = data
}
}
In my tableViewController I call the function after parsing the data:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let meta = chartMetas[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "chartCell", for: indexPath) as! ChartTableViewCell
cell.readings = currentReadings
cell.updateCell()
return cell
}
What did I miss? Since the view is lagging so hard when scrolling.
UPDATE:
I tried, as suggested, to prepare the chart data in the viewcontroller and pass it to the cell. However it seems like the problem in the resuseable cells, the lags appears when I scroll and a new cell enters the screen. How can I fix this? It is pretty annoying.
UPDATE 2:
It looks like the Charts aren't supposed to be used in a tableview...
https://github.com/danielgindi/Charts/issues/3395
To get a serious performance boost, you'll likely need to implement UITableViewDataSourcePrefecting. By doing this, the table view will call into your delegate to let you know that a cell will be needed ahead of time. This will give your code a chance to prepare and render any data it needs.
From the documentation:
You use a prefetch data source object in conjunction with your table
view’s data source to begin loading data for cells before the
tableView(_:cellForRowAt:) data source method is called.The following
steps are required to add a prefetch data source to your table view:
Create the table view and its data source.
Create an object that adopts the UITableViewDataSourcePrefetching
protocol, and assign it to the prefetchDataSource property on the
table view.
Initiate asynchronous loading of the data required for the cells at
the specified index paths in your implementation of
tableView(_:prefetchRowsAt:).
Prepare the cell for display using the prefetched data in your
tableView(_:cellForRowAt:) data source method.
Cancel pending data load operations when the table view informs you
that the data is no longer required in the
tableView(_:cancelPrefetchingForRowsAt:) method.
You chart library is probably doing some pretty heavy processing when there are large data sets. If the author of this library didn't account for this, you are executing this heavy processing on the main thread every time you scroll and a new cell appears on the screen. This will certainly cause stuttering.
I would look to change the cell setup method to simply display a spinner or placeholder table, and then kick off the loading of data and building of the chart on a background thread. This should cause the UITableView scrolling to be smooth, but you will see the cells with placeholders as you scroll through the table, where the charts get populated after the background thread processing completes.
Running things on a background thread in Swift is pretty easy.
Just make sure when you are ready to update the UI with the chart, you do that on the main thread (Al UI updates should be executed on the main thread or you will see weird visual oddities or delays in the UI being updated). So maybe hide the placeholder image and show the chart using animations on the main thread after the heavy processing is done on the background thread.
What if you did something like this?
func updateCell(chartData: ChartData) {
DispatchQueue.global(qos: .background).async {
print("This is run on the background queue")
self.readings = (readings != nil) ? readings!.readings : []
self.period = period
if (chartData.isKind(of: LineChartData.self)) {
data.lineData = chartData as! LineChartData
}
else if (chartData.isKind(of: BarChartData.self)) {
data.barData = chartData as! BarChartData
}
self.chartView.data = data
DispatchQueue.main.async {
// something that triggers redrawing of the chartView.
chartView.fitScreen()
}
}
}
Note that the main thread call is inside the background thread execution block. This means all the stuff will run on the background thread, and when that background thread stuff is done, it will call something in the chart library that will do the UI refresh. Again, this won't help if your chart library is written to force long running operations to happen on the main thread.
Try to prepare all the chart data before you pass it to the cell. I mean
make all the stuff you do I the updateCell() func in your tableViewController probably in viewDidLoad and pass the generated chart Datas to an array.
Then in tableView(tableView: , cellForRowAt indexPath: ) just pass the previously generated data to your cell.
So your updateCell func should looks like so:
func updateCell(lineChartData: LineChartData) {
// SET DATA AND RELOAD
chartView.data = lineChartData
chartView.fitScreen()
}
And you tableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let meta = chartMetas[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "chartCell", for: indexPath) as! ChartTableViewCell
cell.readings = currentReadings
cell.updateCell(lineChartData: arrayOfGeneratedChartData[indexPath.row])
return cell
}
Seems that you keeps entries in the cell, so don't do this. Keep them in the tableViewController. Pass only required data to the cell and don't do hard tasks in the cell methods.

Table view continues to add rows while observing .childAdded even when child was not added in Firebase

I have a UITableView that gets populated by the following firebase database:
"games" : {
"user1" : {
"game1" : {
"currentGame" : "yes",
"gameType" : "Solo",
"opponent" : "Computer"
}
}
}
I load all the games in viewDidLoad, a user can create a new game in another UIViewController, once a user does that and navigates back to the UITableView I want to update the table with the new row. I am trying to do that with the following code:
var firstTimeLoad : Bool = true
override func viewDidLoad() {
super.viewDidLoad()
if let currentUserID = Auth.auth().currentUser?.uid {
let gamesRef =
Database.database().reference().child("games").child(currentUserID)
gamesRef.observe(.childAdded, with: { (snapshot) in
let game = snapshot
self.games.append(game)
self.tableView.reloadData()
})
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if firstTimeLoad {
firstTimeLoad = false
} else {
if let currentUserID = Auth.auth().currentUser?.uid {
let gamesRef = Database.database().reference().child("games").child(currentUserID)
gamesRef.observe(.childAdded, with: { (snapshot) in
self.games.append(snapshot)
self.tableView.reloadData()
})
}
}
}
Lets say there is one current game in the data base, when viewDidLoad is run the table displays correctly with one row. However anytime I navigate to another view and navigate back, viewDidAppear is run and for some reason a duplicate game seems to be appended to the games even though no child is added.
The cells are being populated by the games array:
internal func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("GameTableViewCell", owner:
self, options: nil)?.first as! GameTableViewCell
let game = games[indexPath.row]
if let gameDict = game.value as? NSDictionary {
cell.OpponentName.text = gameDict["opponent"] as? String
}
return cell
}
UPDATE:
Thanks to everyone for their answers! It seems like I misunderstood how firebase .childAdded was functioning and I appreciate all your answers trying to help me I think the easiest thing for my app would be to just pull all the data every time the view appears.
From what I can see, the problem here is that every time you push the view controller and go back to the previous one, it creates a new observer and you end up having many observers running at the same time, which is why your data appears to be duplicated.
What you need to do is inside your viewDidDisappear method, add a removeAllObservers to your gameRef like so :
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
guard let currentUserId = Auth.auth().currentUser?.uid else {
return
}
let gamesRef = Database.database().reference().child("games").child(currentUserId)
gamesRef.removeAllObservers()
}
I cannot see all your code here so I am not sure what is happening, but before adding your child added observer, you need to remove all the elements from your array like so :
games.removeAll()
Actually, as per best practices, you should not call your method inside your ViewDidLoad, but instead you should add your observer inside the viewWillAppear method.
I cannot test your code right now but hopefully it should work like that!
Let me know if it doesn't :)
UPDATE:
If you want to initially load all the data, and then pull only the new fresh data that is coming, you could use a combination of the observeSingleEvent(of: .value) and observe(.childAdded) observers like so :
var didFirstLoad = false
gamesRef.child(currentUserId).observe(.childAdded, with: { (snapshot) in
if didFirstLoad {
// add your object to the games array here
}
}
gamesRef.child(currentUserId).observeSingleEvent(of: .value, with: { (snapshot) in
// add the initial data to your games array here
didFirstLoad = true
}
By doing so, the first time it loads the data, .childAdded will not be called because at that time didFirstLoad will be set to false. It will be called only after .observeSingleEvent got called, which is, by its nature, called only once.
Try following code and no need to check for bool , Avoid using bool here its all async methods , it created me an issue in between of my chat app when its database grows
//Remove ref in didLoad
//Remove datasource and delegate from your storyboard and assign it in code so tableView donates for data till your array don't contain any data
//create a global ref
let gamesRef = Database.database().reference()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.games.removeAllObjects()
if let currentUserID = Auth.auth().currentUser?.uid {
gamesRef.child("games").child(currentUserID)observe(.childAdded, with: { (snapshot) in
self.games.append(snapshot)
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.reloadData()
})
gamesRef.removeAllObserver() //will remove ref in disappear itself
//or you can use this linen DidDisappear as per requirement
}
else{
//Control if data not found
}
}
//TableView Delegate
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.games.count == 0{
let emptyLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
emptyLabel.text = "No Data found yet"
emptyLabel.textAlignment = .center
self.tableView.backgroundView = emptyLabel
self.tableView.separatorStyle = .none
return 0
}
else{
return self.games.count
}
}
observe(.childAdded) is called at first once for each existing child, then one time for each child added.
Since i also encounter a similar issue, assuming you don't want to display duplicate objects, in my opinion the best approach, which is still not listed in the answers up above, is to add an unique id to every object in the database, then, for each object retrieved by the observe(.childAdded) method, check if the array which contains all objects already contains one with that same id. If it already exists in the array, no need to append it and reload the TableView. Of course observe(.childAdded) must also be moved from viewDidLoad() to viewWillAppear(), where it belongs, and the observer must be removed in viewDidDisappear. To check if the array already includes that particular object retrieved, after casting snapshot you can use method yourArray.contains(where: {($0.id == retrievedObject.id)}).

Is it unsafe to call reloadData() after getting an indexPath but before removing a cell at that indexPath?

I'm trying to track down a difficult crash in an app.
I have some code which effectively does this:
if let indexPath = self.tableView.indexPath(for: myTableViewCell) {
// .. update some state to show a different view in the cell ..
self.tableView.reloadData()
// show nice fade out of the cell
self.friendRequests.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .fade)
}
The concern is that calling reloadData() somehow makes the indexPath I just retrieved invalid so the app crashes when it tries to delete the cell at that indexPath. Is that possible?
Edit:
The user interaction is this:
User taps a button [Add Friend] inside of table view cell <-- indexPath retrieved here
Change the button to [Added] to show the tap was received. <-- reloadData called here
Fade the cell out after a short delay (0.5s). <-- delete called here with indexPath from #1
I can change my code to not call reloadData and instead just update the view of the cell. Is that advisable? What could happen if I don't do that?
Personally, I'd just reload the button in question with reloadRows(at:with:), rather than the whole table view. Not only is this more efficient, but it will avoid jarring scrolling of the list if you're not already scrolled to the top of the list.
I'd then defer the deleteRows(at:with:) animation by some small fraction of time. I personally think 0.5 seconds is too long because a user may proceed to tap on another row and they can easily get the a row other than what they intended if they're unlucky enough to tap during the wrong time during the animation. You want the delay just long enough so they get positive confirmation on what they tapped on, but not long enough to yield a confusing UX.
Anyway, you end up with something like:
func didTapAddButton(in cell: FriendCell) {
guard let indexPath = tableView.indexPath(for: cell), friendsToAdd[indexPath.row].state == .notAdded else {
return
}
// change the button
friendsToAdd[indexPath.row].state = .added
tableView.reloadRows(at: [indexPath], with: .none)
// save reference to friend that you added
let addedFriend = friendsToAdd[indexPath.row]
// later, animate the removal of that row
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if let row = self.friendsToAdd.index(where: { $0 === addedFriend }) {
self.friendsToAdd.remove(at: row)
self.tableView.deleteRows(at: [IndexPath(row: row, section: 0)], with: .fade)
}
}
}
(Note, I used === because I was using a reference type. I'd use == with a value type that conforms to Equatable if dealing with value types. But those are implementation details not relevant to your larger question.)
That yields:
Yes, probably what's happening is the table view is invalidating stored index path.
To test whether or not it is the issue try to change data that is represented in the table right before reloadData() is called.
If it is a problem, then you'll need to use an identifier of an object represented by the table cell instead of index path. Modified code will look like this:
func objectIdentifer(at: IndexPath) -> Identifier? {
...
}
func indexPathForObject(_ identifier: Identifier) -> IndexPath? {
...
}
if
let path = self.tableView.indexPath(for: myTableViewCell)
let identifier = objectIdentifier(at: path) {
...
self.tableView.reloadData()
...
if let newPath = indexPathForObject(identifier) {
self.friendRequests.removeObject(identifier)
self.tableView.deleteRows(at: [newPath], with: .fade)
}
}

Populating UITableViewController With Firebase Data Swift, Xcode 7

I am working with swift in Xcode 7. I am totally new to Swift, Xcode, and Firebase. I would like to have three UITableViewControllers in my iOS app. The first two TableView controllers will need dynamic content and the third TableView controller will need static content. I would like for the second and third TableView controllers to display data based on what is pressed on the previous TableView controller. All of the data will come from my Firebase. I have no idea where to start. Please point me in the right direction! Thank you!
This question is broad, in that it asks how to do three different tasks.
I think you'll be better off getting answers if you only ask for one thing at a time.
I can help you with populating a UITableViewController with Firebase.
class MyTableViewController: UITableViewController {
// your firebase reference as a property
var ref: Firebase!
// your data source, you can replace this with your own model if you wish
var items = [FDataSnapshot]()
override func viewDidLoad() {
super.viewDidLoad()
// initialize the ref in viewDidLoad
ref = Firebase(url: "<my-firebase-app>/items")
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// listen for update with the .Value event
ref.observeEventType(.Value) { (snapshot: FDataSnapshot!) in
var newItems = [FDataSnapshot]()
// loop through the children and append them to the new array
for item in snapshot.children {
newItems.append(item as! FDataSnapshot)
}
// replace the old array
self.items = newItems
// reload the UITableView
self.tableView.reloadData()
}
}
}
This technique uses the .Value event, you can also use .ChildAdded, but then you have to keep track of .ChildChanged, '.ChildRemoved', and .ChildMoved, which can get pretty complicated.
The FirebaseUI library for iOS handles this pretty easily.
dataSource = FirebaseTableViewDataSource(ref: self.firebaseRef, cellReuseIdentifier: "<YOUR-REUSE-IDENTIFIER>", view: self.tableView)
dataSource.populateCellWithBlock { (cell: UITableViewCell, obj: NSObject) -> Void in
let snap = obj as! FDataSnapshot
// Populate cell as you see fit, like as below
cell.textLabel?.text = snap.key as String
}
I do it slightly different when I have a UITableViewController, especially for those that can push to another detail view / or show a modal view over the top.
Having the setObserver in ViewDidAppear works well. However, I didnt like the fact that when I looked into a cells detail view and subsequently popped that view, I was fetching from Firebase and reloading the table again, despite the possibility of no changes being made.
This way the observer is added in viewDidLoad, and is only removed when itself is popped from the Nav Controller stack. The tableview is not reloaded unnecessarily when the viewAppears.
var myRef:FIRDatabaseReference = "" // your reference
override func viewDidLoad() {
super.viewDidLoad()
setObserver()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
// only called when popped from the Nav Controller stack
// if I push to another detail view my observer will remain active
if isBeingDismissed() || isMovingFromParentViewController() {
myRef.removeAllObservers()
}
}
func setObserver() {
myRef.observeEventType(.Value, withBlock: { snapshot in
var newThings = [MyThing]()
for s in snapshot.children {
let ss = s as! FIRDataSnapshot
let new = MyThing(snap: ss) // convert my snapshot into my type
newThings.append(new)
}
self.things = newThings.sort{ $0.name < $1.name) } // some sort
self.tableView.reloadData()
})
}
I also use .ChildChanged .ChildDeleted and .ChildAdded in UITableViews. They work really well and allow you use UITableView animations. Its a little more code, but nothing too difficult.
You can use .ChildChanged to get the initial data one item at a time, then it will monitor for changes after that.
If you want all your data at once in the initial load you will need .Value, I suggest you use observeSingleEventOfType for your first load of the tableview. Just note that if you also have .ChildAdded observer you will also get an initial set of data when that observer is added - so you need to deal with those items (i.e. don't add them to your data set) otherwise your items will appear twice on the initial load.

Resources