Swift TableView Contents Disappear and Flash When Reloaded - ios

I'm building a chat app with a table view of various channels that people can chat on.
If a new message (channel) comes in, I update my data store, and then reload the table view. In this particular instance, if a user accepts an invite to a channel; that channel gets added to the data store.
Every time a new channel gets added, this method gets called:
#objc func processChannels() {
if self.airlockStore.unPinnedChannelDataObjects.isEmpty == true {
return
} else {
let pinnedSection = self.tableViewAdaptor!.sections[0] as! TableViewAdaptorSection<ChannelTableViewCell, ChannelDataObject>
let unPinnedSection = self.tableViewAdaptor!.sections[1] as! TableViewAdaptorSection<ChannelTableViewCell, ChannelDataObject>
unPinnedSection.items = airlockStore.unPinnedChannelDataObjects
pinnedSection.items = airlockStore.pinnedChannelDataObjects
}
updateTableView()
}
And subsequently, this one gets called:
#objc func updateTableView() {
tableViewAdaptor?.tableView.isUserInteractionEnabled = true
tableViewAdaptor?.tableView.reloadData()
}
However, I consistently get a flash / the contents of the table view temporarily disappear before then being displayed in the manner expected.
Have read several posts regarding flashes when loading a table view, but haven't been able to solve this yet.
I was looking at reloading individual rows in the table view, but the problem is; there's no guarantee of where the index in my list of channels a new channel will be added to.

Have you tried not reloading after every update of tableview?
On the other hand, it may not be important but why you set isUserInteractionEnabled = true so many times.

Use this before and after adding/deleting row in table view:
tableview.beginupdates()
// adding/deleting row
tableview.endupdates()

Related

Problems with asynchronous data, UITableView, and reloadRowsAt

I'm trying to implement a tableView that has 4 different possible prototype cells. They all inherit from base UITableViewCell class and implement its protocol.
For two of the cells there's asynchronous data fetching but one in particular has been giving me fits. The flow is as follows:
1) Dequeue reusable cell
2) Call configure
func configure(someArguments: ) {
//some checks
process(withArguments: ) { [weak self in] in
if let weakSelf = self {
weakSelf.reloadDelegate.reload(forID: id)
}
}
}
3) If the async data is in the cache, configure the cell using the image/data/stuff available and be happy
4) If the async data is NOT in the cache, fetch it, cache it, and call the completion
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
//add to cache
//call completion
}
}
}
5) If the completion is called, reload the row in question by passing the index path up to the UITableViewController and calling reloadRows(at:with:)
func reload(forID: ) {
tableView.beginUpdates()
tableView.reloadRows(at: indexPath_matching_forID with: .automatic)
tableView.endUpdates()
}
Now, my understanding is that reloadRows(at:with:) will trigger another dataSource/delegate cycle and thus result in a fresh resuable cell being dequeued, and the configure method being called again, thereby making step #3 happy (the async data will now be in the cache since we just fetched it).
Except...that's not always happening. If there are cells in my initial fetch that require reloading, it works - they get the data and display it. Sometimes, though, scrolling down to another cell that requires fetching DOES NOT get the right data...or more specifically, it doesn't trigger a reload that populates the cell with the right data. I CAN see the cache being updated with the fresh data, but it's not...showing up.
If, however, I scroll completely past the bad cell, and then scroll back up, the correct data is used. So, what the hell reloadRows?!
I've tried wrapping various things in DispatchQueue.main.async to no avail.
reloadData works, ish, but is expensive because of potentially many async requests firing on a full reload (plus it causes some excessive flickering as cells come back)
Any help would be appreciated!
Reused cells are not "fresh". Clear the cell while waiting for content.
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
// ** reset the content of the cell, clear labels etc **
//add to cache
//call completion
}
}
}

Clearing Firebase observations from a UITableViewCell

In all iOS classes that use Firebase you will have code like this,
private func clearObservations() {
// your method for clearing observations, probably something like
blah blah. removeAllObservers()
}
In view controllers, it's essential that you call this in viewWillDisappear (or viewDidDisappear)
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
clearObservations()
}
That's fine.
Assume that you have created an observation in a UITableViewCell.
What is the best place in a cell to "clear observations" ?
Note that prepareForReuse is useless, try it.
The only approach we've found is
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
clearObservations()
}
super.willMove(toSuperview: newSuperview)
}
Seems flakey/bizarre though.
What's the deal on this?
Update
Note while "XY Answers" are interesting and informative, if anyone knows the answer to the question that would be great also!
Preface
This was an attempt to answer the question but the question was misunderstood. I'll leave it here as it does have some relevance regarding observers, handles and tableView cell interaction.
While you can go through those gyrations, it's not really needed in most use cases.
For example, if you add and observer to a node, there wouldn't necessarily be a someRef? variable hanging around. So here we are watching the Posts node for new posts
let postsRef = self.ref.child("Posts")
postsRef.observe(.childAdded, with: { snapshot in
print(snapshot) //add the post to the dataSource and reloadTableview/cell
})
Here's another example of watching for any posts that are changed by uid_2
let postsRef = self.ref.child("Posts")
let queryRef = postsRef.queryOrdered(byChild: "poster_id").queryEqual(toValue: "uid_2")
queryRef.observe(.childChanged) { (snapshot) in
print(snapshot) //change the post in the dataSource and reloadTableview/cell
}
No class vars are needed for this functionality and nothing needs be nil'd. The point here being that you do not have to have class vars to get observing functionality and you do not need to keep a handle for every observer (keep reading)
In view controllers, it's essential that you call this
(someRef?.removeAllObservers()) in viewWillDisappear (or Did)..
will use Firebase in the cells of tables.
To clarify; I wouldn't want to put Firebase observers in the cells of tables. The observers should be in whichever viewController controls the tableView that has cells. Cells should pull data from the dataSource array (which is backed by Firebase)
There are some circumstances where you may want to remove all observers, again no need to have a class var or a need to nil a var.
let postsRef = self.ref.child("Posts")
postsRef.removeAllObservers()
There are times when a specific observer needs to be removed (in the case where a node has observers on it's child nodes for example), and in those cases, we store a handle to that observer as say, a class var (keeping them in an array is a tidy way to do it)
class ViewController: UIViewController {
var myPostHandle : DatabaseHandle?
func addObserver() {
let postsRef = self.ref.child("Posts")
self.myPostHandle = postsRef.observe(.childAdded, with: { snapshot in
print(snapshot)
})
func stopObserving() {
if self.myPostHandle != nil {
let postsRef = self.ref.child("Posts")
postsRef.removeObserver(withHandle: self.myPostHandle) //remove only the .childAdded observer
}
}
}
Again though, once the observer is removed, the handle would go out of scope once the class closes.
Tableviews that contain cells are backed by a dataSource and that dataSource get's it's data from firebase. When something is added, changed or removed from Firebase, your app is notified and the array is updated and then the cell refreshed. No need for an observer in the cell itself.
There's no need to add dozens of observers (in the cells) - add one central observer and let it keep the array current. Refresh tableView only when something changes.
EDIT
To Address a comment regarding the use of removeAllObservers: code is worth 1000 words:
Create a new Firebase project with two button actions. Here's the code for button0 which adds an observer to a node:
func button0() {
let testRef = self.ref.child("test_node")
testRef.observe( .value) { snapshot in
print(snapshot)
}
}
when this button0 is clicked, from there on, any adds, changes, or deletes to the test node will print it's contents to the log.
func button1() {
let testRef = self.ref.child("test_node")
testRef.removeAllObservers()
}
This will remove all observers for the node specified. Once clicked, no events will print to the console.
Try it!
It is not right to clear observations in cell and therefore there is not a best place to do it in cell, because, firstly, this approach contradicts MVC pattern. Views only responsible for displaying content and they should only contain code that describes how they must be draw. And in the view controller you give the content for showing by views. Usually content has provided by your model. So controller connects views and model. In your case, when you place clearObservations() in cell class, you also have someRef as a class property, so you have a model in your view class and this is incorrect.
Secondly, if you try to clear observations in table cell you definitely make logic of showing some content in table in wrong way. Cell only show data that has to be generated by some object that conforms to UITableViewDataSource protocol and implements protocol methods. For instance, in cellForRow method you generate cell and setup it with some content from array. This array is generated from model (Firebase service). Your view controller may be this data source object. You have to include array property to controller class and someRef, than you fill array and reload table data. If controller's view disappeared you clear observations, but you do it only inside view controller (in viewWillDisappear()).
Overall, all manipulations with someRef you should do in view controller and therefore "clear observations" also inside controller.

UICollectionView state restoration: restore all UICollectionViewCells

I searched a lot through Google and SO, so please forgive me, if this question has already been answered!
The problem:
I have a UICollectionView with n UICollectionViewCells. Each cell contains a UIView from a XIB file. The Views are used for data entry, so all cells have a unique reuseIdentifier. Each View has also a unique restorationIdentifier. Everything works in normal usage, but not when it comes to state restoration:
The first 3 or 4 cells are getting restored properly because they are visible on the screen on startup, but the remaining cells, which are not visble, are not getting restored.
Current solution:
So I've discovered so far that a View is only restored if it's added to userinterface at startup.
My current working solution is to set the height of all cells to 1 in the process of restoring. Now every cell is loaded and all views are restored.
When applicationFinishedRestoringState() is called, I reload the CollectionView with the correct height.
Now my question is: I'm not happy with this solution, is there a more clean way to achieve restoring of all the UIViews?
I think you are getting a bit confused between your data model and your views. When first initialised, your table view is constructed from a data model, pulling in stored values in order to populate whatever is in each cell. However, your user does not interact directly with the data model, but with the view on the screen. If the user changes something in the table view, you need to signal that change back up to the view controller so that it can record the change to the data model. This means in turn that if the view needs to be recreated the view controller has the information it needs to rebuild whatever was in the table when your app entered the background.
I have put together a simple gitHub repository here: https://github.com/mpj-chandler/StateManagementDemo
This comprises a CustomTableViewController class which manages a standard UITableView populated with CustomTableViewCells. The custom cells contain three switch buttons, allowing the state of each cell to be represented by an array of Boolean values.
I created a delegate protocol for the cells such that if any of the switches is tripped, a signal is sent back to the view controller:
protocol CustomTableViewCellDelegate {
func stateDidChange(sender: CustomTableViewCell) -> Void
}
// Code in CustomTableViewCell.swift:
#objc fileprivate func switched(sender: UISwitch) -> Void {
guard let index : Int = switches.index(of: sender) else { return }
state[index] = sender.isOn
}
// The cell's state is an observed parameter with the following didSet method:
fileprivate var state : [Bool] = Array(repeating: false, count: 3) {
didSet {
if state != oldValue, let _ = delegate {
delegate!.stateDidChange(sender: self)
}
}
}
CustomTableViewController is registered to the CustomTableViewCellDelegate protocol, so that it can record the change in the model as follows:
// Code in CustomTableViewController.swift
//# MARK:- CustomTableViewCellDelegate methods
internal func stateDidChange(sender: CustomTableViewCell) -> Void {
guard let indexPath : IndexPath = tableView.indexPath(for: sender) else { return }
guard indexPath.row < model.count else { print("Error in \(#function) - cell index larger than model size!") ; return }
print("CHANGING MODEL ROW [\(indexPath.row)] TO: \(sender.getState())")
model[indexPath.row] = sender.getState()
}
You can see here that the function is set up to output model changes to the console.
If you run the project in simulator and exit to the home screen and go back again you will see the state of the tableView cells is preserved, because the model reflects the changes that were made before the app entered the background.
Hope that helps.

iOS - loading items when user reach the scroll view bottom

I'm currently using CloudKit as my backend and so far I've been enjoying it pretty much.
I've a query happening, that retrieves data to populate a table view.
Since I don't know the items count that it may have, I'm filtering the query to X number of items (let's say 15).
My goal is that when the user scrolls down to the bottom (last queried item) of the table view ill query the backend to continue filling the table view.
I've searched but couldn't find CloudKit code that does this.
Can somebody give me a hint on how to do it?
Thank you all for the given help.
Best, Ivan.
On IOS I think you have to use UITableViewController or just UITableView.
Add a UIActivityIndicatorView (i.e. spinner) to your UITableViewController. Connect the outlet to the code:
#IBOutlet weak var spinner: UIActivityIndicatorView!
Add a property to your UITableViewController to keep track that you're currently loading more data so that you don't try to do it twice:
var loadingData = false
Start the spinner animating and then call refreshRes():
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if !loadingData && indexPath.row == refreshPage - 1 {
spinner.startAnimating()
loadingData = true
refreshRes()
}
Have refreshRes() run on a background thread. This will allow your table to still move freely. The animated spinner will tell the user that more data is coming. Once your query returns, update the table data on the main thread.
func refreshRes() {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
// this runs on the background queue
// here the query starts to add new 15 rows of data to arrays
dispatch_async(dispatch_get_main_queue()) {
// this runs on the main queue
self.refreshPage += 15
self.tableView.reloadData()
self.spinner.stopAnimating()
self.loadingData = false
}
}
After that it's depend of the server result you have to make a request to GET the others 15 datas

Firebase filling array twice -- Swift

I have two String arrays: one that holds order numbers, and one that holds addresses.
I pull data from Firebase in viewDidAppear using a function that contains the following:
self.localOrderNumberArray.removeAll()
self.localAddressArray.removeAll()
self.orderNumbers.removeAll()
self.addresses.removeAll()
self.tableView.reloadData()
if onItsWayCompanyNameStoreNumberCourierNumberRootRef != nil {
let deliveryRef = onItsWayCompanyNameStoreNumberCourierNumberRootRef.childByAppendingPath("deliveries")
deliveryRef.observeEventType(.ChildAdded, withBlock: { snapshot in
self.orderNumbers.removeAll()
self.addresses.removeAll()
print(snapshot.value.objectForKey("orderNumber"))
let orderNumberPulledFromFirebase = snapshot.value.objectForKey("orderNumber") as! String
self.localOrderNumberArray.insert(orderNumberPulledFromFirebase, atIndex: 0)
let addressPulledFromFirebase = snapshot.value.objectForKey("address") as! String
self.localAddressArray.insert(addressPulledFromFirebase, atIndex: 0)
self.orderNumbers = self.localOrderNumberArray
self.addresses = self.localAddressArray
self.tableView.reloadData()
})
}
The function fills a UITableView with the data pulled from Firebase.
Everything works great when I first run the app. I can add data to Firebase through a different function, and the function above will pull the new data into the UITableView just fine.
However, when I segue to a different view controller (another UITableView, in this case), and then come back to the view that holds the function above, the function fills the order number and address arrays twice when I add new data.
If I segue to the other UITableView a second time, and then come back to view that holds the function above, the function fills the order number and address arrays three times when I add new data. And so on and so on.
It's the strangest thing. I can't figure it out, and it's about to drive me over the edge. Please help.
You are calling deliveryRef.observeEventType in viewDidAppear. viewDidAppear will be called each time the ViewController is presented. So when you segue to other ViewController and comes back, viewDidAppear will be called again and deliveryRef.observeEventType is registered again. So effectively there are two listeners doing the same job in your viewController which will add duplicate data to the array.
You have to implement a logic to do observeEventType only once in the ViewController.

Resources