UITableView reloadData not working properly - ios

I'm working on an app, in which I'm calculating data in a loop and for each cycle I want to publish new section in tableview showing that calculated result. I'm adding results to an array and calling tableView.reloadData(). Problem is, the UI is not updating after each loop, but only after the last loop of the cycle and everything is done.
Some notes:
Delegation and dataSource are connected correctly, as the method is working, just not whenever I want
I also tried dispatching the whole loop into async block
I tried calling the reloadData() alone in an async block (lot's of sources advised to try this)
I tried loads of combinations including functions beginUpdates, endUpdates, reload/insert sections/rows. You get the drift.
When calling reloadData(), numberOfSections method is always called, but the cellForRow only after the whole work is done
For cells I'm using custom cells with UITableViewAutomaticDimension property on the tableView. This ensures that multiline text is shown correctly. I really want to believe my constraints on the cells are fine.
Computation code overview:
override func viewDidAppear(animated: Bool) {
for i in 0..<data.count {
// Do computationally intensive work
results[i].append(result) // multidimensional array
Util.safeInc(&doneCounter) // made thread-safe just in case with objc_sync_enter
resultTableView.reloadData()
}
}
Following are the tableView functions. I have created an expandable tableview. Also have some header functions, to create padding between sections, and selection function. They don't seem to be important here.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if expandedCells.contains(section) {
return results[section].count + 1
} else {
return 1
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return doneCounter
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier("titleCell") as! electionNameTableViewCell
cell.label.text = ...
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("resultCell") as! resultTableViewCell
// set texts for cell labels
return cell
}
}
Any ideas?

You should call resultTableView.reloadData() on the main thread like so :
dispatch_async(dispatch_get_main_queue) {
resultTableView.reloadData()
}

I ended up using my own queue + dispatching reloadData() to the main queue from it.
override func viewDidAppear(animated: Bool) {
let backgroundQueue = dispatch_queue_create("com.example.workQueue", DISPATCH_QUEUE_SERIAL);
for i in 0..<data.count {
dispatch_async(backgroundQueue) {
// Do computationally intensive work
dispatch_async(dispatch_get_main_queue()) {
resultTableView.reloadData()
}
}
}
}

Related

UITableView with Full Screen cells - pagination breaks after fetch more rows

My app uses a UITableView to implement a TikTok-style UX. Each cell is the height of the entire screen. Pagination is enabled, and this works fine for the first batch of 10 records I load. Each UITableViewCell is one "page". The user can "flip" through the pages by swiping, and initially each "page" fully flips as expected. However when I add additional rows by checking to see if the currently visible cell is the last one and then loading 10 more rows, the pagination goes haywire. Swiping results in a partially "flipped" cell -- parts of two cells are visible at the same time. I've tried various things but I'm not even sure what the problem is. The tableView seems to lose track of geometry.
Note: After the pagination goes haywire I can flip all the way back to the first cell. At that point the UITableView seems to regain composure and once again I'm able to flip correctly through all of the loaded rows, including the new ones.
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Pause the video if the cell is ended displaying
if let cell = cell as? HomeTableViewCell {
cell.pause()
}
if let indices = tableView.indexPathsForVisibleRows {
for index in indices {
if index.row >= self.data.count - 1 {
self.viewModel!.getPosts()
break
}
}
}
}
In order to create a "Tik Tok" style UX, I ended up using the Texture framework together with a cloud video provider (mux.com). Works fine now.
I was facing the same issue and as I couldn't find a solution anywhere else here's how I solved it without using Texture:
I used the UITableViewDataSourcePrefetching protocol to fetch the new data to be inserted
extension TikTokTableView: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
viewModel.prefetchRows(at: indexPaths)
}
}
prefetchRows will execute the request if the visible cell is the last one, as in my case
func prefetchRows(at indexPaths: [IndexPath]) {
if indexPaths.contains(where: isLastCell) {
getPosts(type: typeOfPosts, offset: posts.count, lastPostId: lastPost)
}
}
private func isLastCell(for indexPath: IndexPath) -> Bool {
return indexPath.row == posts.count - 1
}
I have a weak var view delegate type TikTokTableViewDelegate in my view model to have access to a function insertItems implemented by my TikTokTableView. This function is used to inform the UITableView where to insert the incoming posts at
self.posts.append(contentsOf: response.posts)
let indexPathsToReload = self.calculateIndexPathToReload(from: response.posts)
self.view?.insertItems(at: indexPathsToReload)
private func calculateIndexPathToReload(from newPosts: [Post]) -> [IndexPath] {
let startIndex = posts.count - newPosts.count
let endIndex = startIndex + newPosts.count
print(startIndex, endIndex)
return (startIndex..<endIndex).map { IndexPath(row: $0, section: 0) }
}
and this is the insertItems function implemented in TikTokTableView and here is the key: If we try to insert those rows, the pagination of the table will fail and leave that weird offset, we have to store the indexPaths in a local property and insert them once the scroll animation has finished.
extension TikTokTableView: TikTokTableViewDelegate {
func insertItems(at indexPathsToReload: [IndexPath]) {
DispatchQueue.main.async {
// if we try to insert rows in the table, the scroll animation will be stopped and the cell will have a weird offset
// that's why we keep the indexPaths and insert them on scrollViewDidEndDecelerating(:)
self.indexPathsToReload = indexPathsToReload
}
}
}
Since UITableView is a subclass of UIScrollView, we have access to scrollViewDidEndDecelerating, this func is triggered at the end of a user's scroll and this is the time when we insert the new rows
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if !indexPathsToReload.isEmpty {
tableView.insertRows(at: indexPathsToReload, with: .none)
indexPathsToReload = []
}
}

Swift Execution Order ViewController UIKit

I'm new to swift and want to use the data from an api in form of a json file. I'm using the TableViewController but the execution order in it seems to be jumping around between the functions and not executing them fully. To see in which order the execution is happening I inserted some print statements.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var Info = Array<String>()
override func viewDidLoad() {
super.viewDidLoad()
parse() { result in
print("parse")
if result != nil {
self.Info = result!
tableView.reloadData()
}
else {return}
}
print("viewDidLoad")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("numberOfRows")
//
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("cellForRowAt")
//
}
}
The result of executing it is:
viewDidLoad
numberOfRows
numberOfRows
parse
So my Question would be:
How do i get the jsonParse completionHandler to execute before numberofRows and why is cellForRowAt not executing ?
Thank you in advance.
You can't because its the completion handler you are calling. But you can again call the tableview numberOfRows and another delegate after you parse data. Just add tableview.reloadData() like this
parse() { result in
DispatchQueue.main.async {
print("parse")
if {
tableview.reloadData()
}
else {return}
}
}
The order is correct. The single steps are
First "viewDidLoad" is printed because parse works asynchronously.
Then the framework calls tableView.reloadData() once implicitly. This causes to print "numberOfRows" (sometimes more than once). At this moment the data source array is empty so cellForRowAt is not going to be called.
Meanwhile parse has finished its job, the closure is executed and "parse" is printed.
Finally the explicit tableView.reloadData() is executed and updates the UI with the received data. If parse is performed on a background thread you have to reload the table view on the main thread
DispatchQueue.main.async {
self.tableview.reloadData()
}

Reloading table causes flickering

I have a search bar and a table view under it. When I search for something a network call is made and 10 items are added to an array to populate the table. When I scroll to the bottom of the table, another network call is made for another 10 items, so now there is 20 items in the array... this could go on because it's an infinite scroll similar to Facebook's news feed.
Every time I make a network call, I also call self.tableView.reloadData() on the main thread. Since each cell has an image, you can see flickering - the cell images flash white.
I tried implementing this solution but I don't know where to put it in my code or how to. My code is Swift and that is Objective-C.
Any thoughts?
Update To Question 1
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(R.reuseIdentifier.searchCell.identifier, forIndexPath: indexPath) as! CustomTableViewCell
let book = booksArrayFromNetworkCall[indexPath.row]
// Set dynamic text
cell.titleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.authorsLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)
// Update title
cell.titleLabel.text = book.title
// Update authors
cell.authorsLabel.text = book.authors
/*
- Getting the CoverImage is done asynchronously to stop choppiness of tableview.
- I also added the Title and Author inside of this call, even though it is not
necessary because there was a problem if it was outside: the first time a user
presses Search, the call for the CoverImage was too slow and only the Title
and Author were displaying.
*/
Book.convertURLToImagesAsynchronouslyAndUpdateCells(book, cell: cell, task: task)
return cell
}
cellForRowAtIndexPath uses this method inside it:
class func convertURLToImagesAsynchronouslyAndUpdateCells(bookObject: Book, cell: CustomTableViewCell, var task: NSURLSessionDataTask?) {
guard let coverImageURLString = bookObject.coverImageURLString, url = NSURL(string: coverImageURLString) else {
return
}
// Asynchronous work being done here.
task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue(), {
// Update cover image with data
guard let data = data else {
return
}
// Create an image object from our data
let coverImage = UIImage(data: data)
cell.coverImageView.image = coverImage
})
})
task?.resume()
}
When I scroll to the bottom of the table, I detect if I reach the bottom with willDisplayCell. If it is the bottom, then I make the same network call again.
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row+1 == booksArrayFromNetworkCall.count {
// Make network calls when we scroll to the bottom of the table.
refreshItems(currentIndexCount)
}
}
This is the network call code. It is called for the first time when I press Enter on the search bar, then it is called everytime I reach the bottom of the cell as you can see in willDisplayCell.
func refreshItems(index: Int) {
// Make to network call to Google Books
GoogleBooksClient.getBooksFromGoogleBooks(self.searchBar.text!, startIndex: index) { (books, error) -> Void in
guard let books = books else {
return
}
self.footerView.hidden = false
self.currentIndexCount += 10
self.booksArrayFromNetworkCall += books
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
If only the image flash white, and the text next to it doesn't, maybe when you call reloadData() the image is downloaded again from the source, which causes the flash. In this case you may need to save the images in cache.
I would recommend to use SDWebImage to cache images and download asynchronously. It is very simple and I use it in most of my projects. To confirm that this is the case, just add a static image from your assets to the cell instead of calling convertURLToImagesAsynchronouslyAndUpdateCells, and you will see that it will not flash again.
I dont' program in Swift but I see it is as simple as cell.imageView.sd_setImageWithURL(myImageURL). And it's done!
Here's an example of infinite scroll using insertRowsAtIndexPaths(_:withRowAnimation:)
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var dataSource = [String]()
var currentStartIndex = 0
// We use this to only fire one fetch request (not multiple) when we scroll to the bottom.
var isLoading = false
override func viewDidLoad() {
super.viewDidLoad()
// Load the first batch of items.
loadNextItems()
}
// Loads the next 20 items using the current start index to know from where to start the next fetch.
func loadNextItems() {
MyFakeDataSource().fetchItems(currentStartIndex, callback: { fetchedItems in
self.dataSource += fetchedItems // Append the fetched items to the existing items.
self.tableView.beginUpdates()
var indexPathsToInsert = [NSIndexPath]()
for i in self.currentStartIndex..<self.currentStartIndex + 20 {
indexPathsToInsert.append(NSIndexPath(forRow: i, inSection: 0))
}
self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: .Bottom)
self.tableView.endUpdates()
self.isLoading = false
// The currentStartIndex must point to next index.
self.currentStartIndex = self.dataSource.count
})
}
// #MARK: - Table View Data Source Methods
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel!.text = dataSource[indexPath.row]
return cell
}
// #MARK: - Table View Delegate Methods
func scrollViewDidScroll(scrollView: UIScrollView) {
if isLoading == false && scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height {
isLoading = true
loadNextItems()
}
}
}
MyFakeDataSource is irrelevant, it's could be your GoogleBooksClient.getBooksFromGoogleBooks, or whatever data source you're using.
Try to change table alpha value before and after calling [tableView reloadData] method..Like
dispatch_async(dispatch_get_main_queue()) {
self.aTable.alpha = 0.4f;
self.tableView.reloadData()
[self.aTable.alpha = 1.0f;
}
I have used same approach in UIWebView reloading..its worked for me.

UITableView not displaying correct amount of rows

I've created a table view within a view controller and am trying to set the amount of rows it will have. The amount gets returned from a database and then passed on to the correct method (see below). The problem is that the amount of rows visible is not the same amount that gets returned from the database.
Within .viewDidLoad()
self.activeIDs.delegate = self
self.activeIDs.dataSource = self
self.activeIDs.rowHeight = 30
self.activeIDs.reloadData()
Methods that are supposed to "set up" the table view
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = activeIDs.dequeueReusableCellWithIdentifier("idCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel!.text = "Test"
return cell
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
var numberOfRows = 0 //Assigning default value
SQLHandler.getActiveIDAmount {
amountOfIDs in
numberOfRows = amountOfIDs.description.toInt()!
println(numberOfRows) //Displays correct (database) value
}
return numberOfRows //Returns correct value EDIT: wrong value.
}
Instead of getting the desired amount of rows (4) I always, despite the value which I get from the database, end up with 6? Screenshot of table view in action: http://gyazo.com/753f326177dc8cd6b1734f4d19681d71
What is the problem? What am I doing wrong?
The method you are calling SQLHandler is a completion handler, that means that swift will continue executing your code and just after (and return the numberOfRowns = 0) than after (when the the request finish) it will come back to the block:
SQLHandler.getActiveIDAmount {
amountOfIDs in
numberOfRows = amountOfIDs.description.toInt()!
println(numberOfRows) //Displays correct (database) value
//add the values returned to your dataset here
//call refresh table and dispatch in the main thread in case
//this block is running in a background thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
and print the number of rows.
What you need to do is to call the function SQLHandler.getActiveIDAmount somewhere else in your code and call table.reloadData() after the callback is finished.
I hope that helps you!

self.tableView.reloadData() not reloading

I have a UITableViewController which works fine with TableViewCell:
class NewTableViewController: UITableViewController {
However I want to implement a method to update and reload the data in the TableView.
The update part works well, it deletes CoreData objects, queries HealthKit, saves to CoreData and then call a function (func setupArrays) in TableViewController which fetch data from CoreData and appends it to arrays used in cellForRowAtIndexPath. From the Console log I can see that it works well(e.g. the updated arrays reflects changes in Healthkit). The problem arises when I try to reload the tableView:
self.tableView.reloadData()
nothing happens !
I did some research and also tried this method:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
Still nothing.
I call the reloadData function at the end of the function setupArrays. (This is also the place where I print to the log the arrays which correctly reflect changes):
func setupArrays (){
if NSUserDefaults.standardUserDefaults().boolForKey("stepsSwitch") == true {
titleArray.append(stepsCell.title())
imageNameArray.append(stepsCell.imageName)
xAxisDatesArray.append(cdFetchSteps.queryCoreDataDate())
yAxisValuesArray.append(cdFetchSteps.queryCoreDataData())
}
if NSUserDefaults.standardUserDefaults().boolForKey("hrSwitch") == true {
titleArray.append(heartRateCell.title())
imageNameArray.append(heartRateCell.imageName)
xAxisDatesArray.append(cdFetchHeartRate.queryCoreDataDate())
yAxisValuesArray.append(cdFetchHeartRate.queryCoreDataData())
}
if NSUserDefaults.standardUserDefaults().boolForKey("weightSwitch") == true {
titleArray.append(weightCell.title())
imageNameArray.append(weightCell.imageName)
xAxisDatesArray.append(cdFetchWeight.queryCoreDataDate())
yAxisValuesArray.append(cdFetchWeight.queryCoreDataData())
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
println(yAxisValuesArray)
}
Both delegate and dataSource is set correctly in the IB. I tried to add them "explicitly" in the class e.g.:
class NewTableViewController: UITableViewController, UITableViewDataSource, UITableViewDelegate{
But that did nothing.
UPDATE 1
Here is my numberOfRowsInSection and cellForRowAtIndexPath:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return titleArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var myCell:TableViewCell = tableView.dequeueReusableCellWithIdentifier("myCell") as TableViewCell
myCell.title.text = titleArray[indexPath.row]
myCell.imageName = imageNameArray[indexPath.row]
myCell.xAxisDates = xAxisDatesArray[indexPath.row]
myCell.yAxisValues = yAxisValuesArray[indexPath.row]
return myCell }
UPDATE 2
when I put a breakpoint cellForRowAtIndexPath it shows the values for title and imageName, but appears empty for xAxisDates and xAxisValues both at the first load and after reload (see attached picture). This seems strange to me as the values are available in the TableViewCell and displays fine. Are arrays not visible in the debug area/variables view ?
Question: How do I update my TableViewController?
Any help would be very much welcomed - thank you.

Resources