tableView reloadData in loop not drawing cells on each pass - ios

I have a tableView.reloadData() call in a loop in several places in my app. the basic idea is like this:
let dataSourceArray = []
for arrayItem in anotherArray {
dataSourceArray.append(arrayItem)
self.tableView.reloadData()
}
What I'm trying to achieve is to have the tableView draw each cell as it is loaded into the model array.
What is currently happening is that all the cells appear once the loop is complete.
This is all being done in the main thread, however I tried anyway to dispatch to main thread for the reloadData call. That made no difference. I tried tableView.layoutIfNeeded() after reloadData and that also made no difference.
Placing break points in the cellForRowAtIndexPath and heightForRow... methods indicate normal tableView calls as expected at each pass in the loop. However for some reason the cells don't draw on screen until the loop is completed.
Is there anyway to force the tableView to draw the cell at each pass other than what I have tried above?

I belive the reason is because the looping of your elements will be done very quickly (milliseconds) therefore you will not notice the incremental changes.

You need to run outer loop in global queue like below and dispatch reloadData to main queue.
var dataSourceArray = [ String ]()
#IBAction func
DoA( _: Any? ) {
let anotherArray = [ "A", "B", "C" ]
DispatchQueue.global().async {
for arrayItem in anotherArray {
self.dataSourceArray.append( arrayItem )
DispatchQueue.main.async {
self.tableView.reloadData()
}
Thread.sleep( forTimeInterval: 1 )
}
}
}

If you want a neat animation thats visible then you can probably force a timer on it, between each append; with a UIView.animate or UIView.transition... I'd prefer the transition method, it gives you a few options that make your changes even neater.
Here is a sample of what you can do (dont copy paste it directly, adjust it to your own, i wrote it in playground without testing it)
var i = 0
func addItem(){
if i == anotherArray.count {
return
}
UIView.transition(with: tableView, duration: 0.3, options: [.allowAnimatedContent, .transitionCrossDissolve], animations: { // content that is going to animate
dataSourceArray.append(anotherArray[i])
tableView.reloadData()
}) { // completion block
i += 1
addItem()
}
}
Other than that, you can change the content mode of your tableView (most probably from the storyboard) to be set to .top so on every change your tableView keeps on sticking to the top rather than jumping around (https://developer.apple.com/documentation/uikit/uiview/1622619-contentmode). This specially helps if you're using autolayouts

Related

UITableView section header does not always draw

This is a problem stumping me and my team at work.
We have a header view we are using in one section that is not always drawing. It's inconsistent, though appears to be slightly more frequent on fresh installs.
I will first preempt by stating we are not registering it to the table view and thus not actually dequeueing it. I tried that but the boss says since we are never actually re-using it he is adamant against doing so. Therefore we have it like this:
class DashboardViewController {
...
var trendsHeader: TrendsHeader?
...
override func viewDidLoad() {
super.viewDidLoad()
setupTrendsHeader()
....
}
...
func setupTrendsHeader() {
trendsHeader = Bundle(for: TrendsHeader.self).loadNibNamed(TrendsHeader.identifier, owner: view, options: nil)?.first as? TrendsHeader
trendsHeader?.delegate = self
trendsHeader?.datasource = self
trendsHeader?.leftDropdown.selectRowWithAction(at: 0)
}
...
// in heightForHeaderInSection
return TrendsHeader.cellHeight //Height is returned properly, empty space is of the right height
...
// in viewForHeaderInSection
return trendsHeader ?? UIView()//Should only return UIView if trendsHeader is nil
}
I have tried modifying viewForHeaderInSection as such:
//in viewForHeaderInSection
if trendsHeader == nil {
setupTrendsHeader()//Breakpoint inserted here
}
return = trendsHeader ?? UIView()
And with a breakpoint in the if statement so I should know if the trendsHeader is nil. It doesn't hit the breakpoint but still doesn't draw that header. If I wait anywhere from 5–30 seconds the header will show up, or if I scroll down the header will show up once it redraws the section. But I need it to show up initially as well, which it still does in most runs of the app but sometimes just doesn't. Honestly the most frustrating part is the inconsistency.
Any insight as to why this is occurring and/or a resolution that doesn't involve convincing my boss to register a view for re-use that isn't going to be re-used?
We found it. Long story short, the section in question is tied to 2 service calls, one for each option on the header's drop down menu. Those services had delegates who reloaded the section upon the callback functions, and they were getting them in quick succession causing the glitch.
We replaced the reload section code with reload rows code instead, so the header does not get refreshed.

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
}
}
}

Refresh Control Issue with custom cells - Swift 4 IOS 11

I am building an iOS app and I am trying to implement a pull-down refresh control on my project. The data is fetched correctly from an API and displayed on my table. But the problem rises when I do pull down to refresh. The following situations happen:
If I pull down for a long distance from the top, and the tableview.reloadData() function is called, the cells in the non-visible portion of the table come with the default tableview cells on top of them, overlapping...
if I pull down multiple times in quick succession the same issue happens.
I believe that it is because tableview.reloadData() is called multiple times in quick succession. But why are the default cells getting dequeued on top of my custom cells? Here is the section of code in the function to handle the pulldown:
#objc func refreshFunc(){
//let offset = scrollView.contentOffset.y
if myRefreshControl.isRefreshing{
readJson { (activities) in
self.activities = activities
self.tableView.reloadData()
self.myRefreshControl.endRefreshing()
}
}
}
Any help would be appreciated. Thanks in advance
UPDATE:
Changing the code to the code below seems to remove the error, but the problem is that now I need to pull down twice in order to get the results updated on the table:
#objc func refreshFunc(){
readJson { (activities) in
self.activities = activities
}
self.tableView.reloadData()
self.myRefreshControl.endRefreshing()
}
Please note that running the reloadData on the main thread gives the same result, I still need to pull down twice to update.
Please try to give some delay before refresh table view may it resolve your problem.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayTime) { [weak self] in
self?.tableView.reloadData()
}
Hope it works
Cheers :)
DispatchQueue.main.async {
self.tableView.reloadData
}
Try this, always, if u want change UI you have to call on main thread

Why my cells do not load at the same time?

I'm using background thread for getting my data from the network and main thread for presenting my cells:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), {
let _ = self.http.getInfo() { (result) in
self.cellsArray = result
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
completion(completed: true)
}
})
where self.http.getInfo is a NSURLSession.sharedSession().dataTaskWithRequest method for taking my network infos.
And when I run my app my cells start showing separately as: the first appears, in a second the next one appears, in 2 seconds the next one and etc.
Why they do not appear at the same time? And also, because of the background and main thread using, my UITableView jumps when I implement infinite scroll and append new cells to exist cell array.
How can I fix it?
write self.tableView.reloadData() this statement in completion handler of NSURLSession.sharedSession().dataTaskWithRequest.

chaining UITableview Updates on main thread

So what i am attempting to do is conceptually very simple however I have not been able to find a solution for it:
I am trying to remove cells from a tableView animated with the:
self.coolTableView?.deleteRowsAtIndexPaths
function, to do this I change the dataSet and perform this action, right after it is done i would like to change the data set again and use:
self.coolTableView?.insertRowsAtIndexPaths
to reflect and animate the second change to the dataset.
The Problem I run into is that if I use:
dispatch_async(dispatch_get_main_queue()) { () -> Void in
//Update tableview
}
they seem to lock each other out, the used memory just keeps skyrocketing and it hangs. I am assuming they are interfering with each other. now my next thought was to change the code to sync so:
dispatch_sync(dispatch_get_main_queue()) { () -> Void in
//Update tableview
}
however the first update hangs and and ceases operation. With the research I have done it sounds like I am putting the execution of the block in the main queue behind my current running application and vuwala, that is why it hangs.
what is the correct way to block execution until I can complete an animation on the main thread so i do not blow up my data source before animations can take place?
The animations in iOS take a block that they can execute when the animation terminates. Try putting operations on coolTableView into a closure (remember about unowned self to not create memory leaks) and pass it as completion closure to your animation.
example:
let someAnimations: () -> Void = {
//some animations
}
let manipulateTableView: (Bool) -> Void = {
isAnimationFinished in
// perform some data manipulation
}
UIView.animateWithDuration(0.5, animations: someAnimations, completion: manipulateTableView)

Resources