Improve TableView Scroll by Moving code to willDisplayHeaderView - ios

My tableview is not scrolling smoothly. I have seen this comment from apple.
But very important thing is still there: tableView:cellForRowAtIndexPath: method, which should be implemented in the dataSource of UITableView, called for each cell and should work fast. So you must return reused cell instance as quickly as possible.
Don’t perform data binding at this point, because there’s no cell on
screen yet. For this you can use
tableView:willDisplayCell:forRowAtIndexPath: method which can be
implemented in the delegate of UITableView. The method called exactly
before showing cell in UITableView’s bounds.
From Perfect smooth scrolling
So I am trying to implement all my code in viewForHeaderInSection to willDisplayHeaderView (Note, I am using sections rather than rows for this specific example because I have custom sections). However, I am getting a "fatal error: unexpectedly found nil while unwrapping an Optional value" error at
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
Below are my original and attempted code that crashed
Original (Note, this code works fine with some minor scrolling lagging problem that I am trying to improve)
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier("TableSectionHeader") as? TableSectionHeader {
// Cancel request of current cell if there is a request going on to prevent requesnt info from background from previous use of same cell
cell.AlamoFireRequest?.cancel()
var image: UIImage?
if let url = post.imageUrl {
image = DiscoverVC.imageCache.objectForKey(url) as? UIImage
}
cell.configureCell(post) // This is data binding part
cell.delegate = self
return cell
} else {
return TableSectionHeader()
}
}
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
}
Attempt
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier("TableSectionHeader") as? TableSectionHeader {
return cell
} else {
return TableSectionHeader()
}
}
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
// Cancel request of current cell if there is a request going on to prevent requesnt info from background from previous use of same cell
cell.AlamoFireRequest?.cancel()
var image: UIImage?
if let url = post.imageUrl {
image = DiscoverVC.imageCache.objectForKey(url) as? UIImage
}
cell.configureCell(post) // This is data binding part
cell.delegate = self
}
-----Update to address Michael's answer------
As there are word limits on replies, here is a response to the Answer from Michael
You are correct, I have updated where I got the snippet from in my questions. My mistake
I agree that problem could very well be lying else where, however this is something that I am going through at the moment. My tableview scrolls OK but sometimes when there is image it slows down a little. So I am going through some steps to ellimate any potential cause.
The reason that I specifically didnt use if let here is that becuase I was expecting the cell to be displayed will be TableSectionHeader. I tried to add it in just then and I ALWAYS gets a failed cast.
The reason that I subclass UITableViewHeaderFooterView is because my headerview is a Xib file where I have func configureCell() so I could call cell.configureCell. (And many other functions)
My header includes a few items like
labels to display title, date, time downloaded from firebase
image that can be optional
image description
like btn, commentbtn, more, btn
All of theses function are addressed in my TableSectionHeader.swift which inherits from UITableViewHeaderFooterView
Could you please explain what you mean by "It's looking suspiciously like you're trying to store state in the header - you should store state outside the tableView."?
Reason that I am cancelling Alamofire request here is because the cell gets dequeued. So if the user scrolls really fast, the cell would get many alamofire request. So I cancelled it first and re-open a download request (inside cell.configureCell) if I dont have anything in my cache
I am not sure how printing sections would help identify. I am thinking it is something foundamentally wrong that I am doing here putting everything in willDisplayHeaderView code (As most place you would put it in viewForHeaderInSection instead). Or maybe it is just the syntax

You're given the header view in the method, you just need to cast it. Try this:
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
guard let cell = view as? TableSectionHeader else { return }
cell.AlamoFireRequest?.cancel()
...
}

I don't think that comment comes from Apple, and I think your problem probably lies elsewhere (eg. blocking the main queue with a task that should run in the background). If you post the contents of TableSectionHeader, this may become clearer.
Pressing on regardless, you are getting a nil value exception in the following line:
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
This is because you're forcing the cast as TableSectionHeader, and it undoubtedly isn't one. If you change it to an if let or guard let, you will see a different code path.
Since you obviously expect it to always be of that type (after all, that is what you're creating in viewForHeaderInSection), something is happening that you don't realise (why are you subclassing UITableViewHeaderFooterView anyway?). I'd be inclined to print the section number and view in both sections so you know what is really happening. It's looking suspiciously like you're trying to store state in the header - you should store state outside the tableView.
Also, there should be no need to cancel the Alamofire request, as this is not the cause of a performance problem - retrieving an image should just fire off another request. Otherwise while a user scrolls around, no images will be displayed because the requests will keep getting cancelled. They would have to leave the screen alone and wait for those images to load before scrolling elsewhere.

Related

How do I use placeholder API on UIElements correctly

I'm implementing an API called Skeleton View on my Xcode Project. Sometimes when the app loads for the first time, it takes a little bit longer to load the UI Elements, because the data come from JSON and if the user is using 4G or even a bad internet, will remain an empty field. In additional, using this API it shows like a gray view animated placeholder on each UIElement that doesn't received data.
However, I don't know how to check when the UIImage received a value to be able to remove the Skeleton effect and present the Image. I'll post some pictures below.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellID") as! TableViewCell
let model = arrCerveja[indexPath.row]
cell.labelName.text = model.name
cell.labelDetail.text = "Teor alcoólico: \(model.abv)"
let resource = ImageResource(downloadURL: URL(string: "\(model.image_url)")!, cacheKey: model.image_url)
if cell.imageViewCell.image != nil {
cell.imageViewCell.kf.setImage(with: resource)
cell.imageViewCell.hideSkeleton()
}else{
cell.imageViewCell.showAnimatedGradientSkeleton()
//The placeholder still there even when the images already got downloaded
//What I wanted was, while the UIImageView has no value, the placeholder
//persists, but when receive the image the placeholder should be dismissed.
}
return cell
}
That's what I got on UIImages(it has a blur animation passing by):
The problem I'm facing is how do I dismiss this effect after each image load itself?
You will need to start the animation when starting the download (often just before your function that get the data) with:
view.startSkeletonAnimation()
The view property here is the one of your view controller, SkeletonView is recursive and will animate all views inside it.
And then after the download is completed (often in a closure before you reload your table view and I assume that all your images are downloaded and ready to be displayed) stop the animation with:
self.view.stopSkeletonAnimation()
Skeleton view also provide you a delegate for UITableView:
public protocol SkeletonTableViewDataSource: UITableViewDataSource {
func numSections(in collectionSkeletonView: UITableView) -> Int
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
func collectionSkeletonView(_ skeletonView: UITableView, cellIdenfierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
}
As the documentation says (read the part about Collections to understand how it works):
This protocol inherits from UITableViewDataSource, so you can replace this protocol with the skeleton protocol.
You will need to make your tableView skeletonnable in you viewDidLoad() function:
tableView.isSkeletonable = true

how to Reload data on table view without crashing in swift 3?

I have a TableView in my app that when user tap the button in one of the cells the safari will open in the app and when the safari dismiss the app will crash because I want to update one of the parameters in the tableview so I need to remove all and append them again from Json But this will take some seconds so the app will crash because there is no data to show I used Timer But timer is not good solution because some times it will take more time than timer so it will crash again I need a method that wait to finish and then do some thing else of if you think this is not a good solution too please guide me which solution is good for this problem
here is what I used in my app after dismiss safari in application(its not all of the codes but these codes has problems )
if let payed = dict.value(forKey: "payed"){
factorViewController.payed.removeAll()
self.factorTableView.reloadData()
factorViewController.payed.append(payed as! Int)
self.factorTableView.reloadData()
print("payed = \([payed])")
}
Try reloading the data only once:
if let payed = dict.value(forKey: "payed") {
// Empty Array here
factorViewController.payed.removeAll()
// You are appending here, so array will not be empty
factorViewController.payed.append(payed as! Int)
// Now you can reload data
self.factorTableView.reloadData()
print("payed = \([payed])")
// Also, if this doesn't work, you can use a callback(completion handler).
}
That way, your array will not be empty when you need to reload data
How TableView Works
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return your_array.count // It returns how much cells you want in your tableview.
}
// This func defines what you wanted in your cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = dequeue_property // to dequeue your cell
your_lbl.text = your_array[indexpath.row] // as per logic
return cell
}
In your case
Count of array in numberOfRowsInSection is more than the array you'r using in the cellForRowAt so on dequeue your cell have more index than the array you are using in cellForRow. Thats why is doesn't getting index and crashes with IndexOutOFRange
You have array issue not tableView Reload issue
Take ref : Swift array always give me "fatal error: Array index out of range"
Ok That was easier than I thought ! I just defined another variables in my app and when started getting Json append new data to the new variables and then equals new variables to old variables ! thats it!

Having an issue grabbing the index of selected viewtable in swift

I have been learning swift through the last few days and I have come across an error that I have been stuck on for quite a while now.
I am attempting to get the selected indexPath so that I can then push data according to which item he selected. I have searched through and tried many different solutions I have found on stack overflow as well as different websites but I am not able to get this figured out still.
The code is below:
#IBOutlet var selectGroceryTable: UITableView!
/* Get size of table */
func tableView(tableView: UITableView, numberOfRowsInSection: Int) ->Int
{
return grocery.count;
}
/* Fill the rows with data */
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let myCell:UITableViewCell = selectGroceryTable.dequeueReusableCellWithIdentifier("groceryListRow", forIndexPath:indexPath) as! UITableViewCell
myCell.textLabel?.text = grocery[indexPath.row];
myCell.imageView?.image = UIImage(named: groceryImage[indexPath.row]);
return myCell;
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
print("Row Selected");
NSLog("Row Selected");
}
Nothing ever prints acting like the function is not being called. However, I do not understand why this would not be called?
Any help would be greatly appreciated!
UPDATE:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
selectGroceryTable.data = self;
selectGroceryTable.delegate = self; //gives error states you can not do this
}
There are a couple of things to check in cases like this:
First, what kind of method is didSelectRowAtIndexPath?
Answer: It's a UITableViewDelegate method. Did you set your view controller up as the delegate of the table view? If not, this method won't get called.
Second, have you made absolutely certain that the method signature is a perfect match for the method from the protocol? A single letter out of place, the wrong upper/lower case, a wrong parameter, and it is a different method, and won't be called. it pays to copy the method signature right out of the protocol header file and then fill in the body to avoid minor typos with delegate methods.
It looks to me like your method signature is correct, so my money is on forgetting to set your view controller up as the table view's delegate.

Custom Cells Not Populating Correct Data When Off-Screen

Forgive the naive nature of this question, but despite my searching here, I can't quite find a solution.
In my project, I have a UITableView with custom cells, all of which are populated by data from an API. At this point, based on the desired design, four of the cells are visible on-screen at launch, and I have to scroll to see the other four. Each cell has two components; a label and a UIView, which is generating a line chart. The first four cells all work properly, but the second set of cells (which are off-screen at loading) populate the label correct, but the UIView is not showing the correct set of data (it is populating with previously used data).
Oddly, if I tap on one of the problematic cells, the cell immediately reloads with the correct data.
For context, here's part of my TableViewController;
...
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return minions.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return minionCellAtIndexPath(indexPath)
}
func minionCellAtIndexPath(indexPath:NSIndexPath) -> MinionCell {
let minion = minions[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier(minionCellIdentifier, forIndexPath: indexPath) as! MinionCell
if let name = minion.name {
cell.nameLabel?.text = minion.name ?? "Minion"
cell.lineChart?.graphPoints = minion.bandwidth ?? [1,2,3,4,5]
} else {
cell.nameLabel?.text = "No data available."
cell.lineChart?.graphPoints = [1,2,3,4,5]
}
return cell
}
...
Is it possible that I need to somehow "pre-render" the off-screen cells at load? Again, sorry for the naive nature.
As always, thank you for your time!
As you most probably know, cells that go off screen get reused for the new rows. That is why the new rows come with the old graphs. The fact that if you tap the cell the graph refreshes and is correctly displayed makes me think that it is a drawing problem. Try triggering a force redraw by calling setNeedsDisplay on the view with the graph:
cell.lineChart?.graphPoints = [1,2,3,4,5]
cell.lineChart?.setNeedsDisplay()
I am somewhat skeptical about your function minionCellAtIndexPath.
The reason you are getting older data again, and not newer is probably because you are not resetting it.
Before your if let checks, set hardcoded values, the ones that you are setting in your else. and then run again.

UITableViewCells not displaying first time

I'm trying to create an autocompleter using iOS 8, Swift and Xcode 6.3
I have a problem that I'm trying to solve, but I gave up... I hope someone can help here. The problem is that (custom) UITableViewCell's are not displaying when the initial dataSource is empty. When adding data to datasource and reloading the tableView, the cells SHOULD display, but they don't... At least, the first time they don't... A second time, they DO... When I initialize the table with non-empty data, the problem doesn't occur. I guess something goes wrong with dequeueReusableCellWithIdentifier. In beginning, no reusable cells are found, or something. But I don't know why...
Relevant code, in ViewController.swift:
// filteredWords is a [String] with zero or more items
#IBAction func editingChanged(sender: UITextField) {
autocompleteTableView.hidden = sender.text.isEmpty
filteredWords = dataManager.getFilteredWords(sender.text)
refreshUI()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! AutocompleteTableViewCell
cell.title.text = filteredWords[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredWords.count
}
func refreshUI() {
self.autocompleteTableView.reloadData()
}
I created a sample project on github:
https://github.com/dirkpostma/swift-autocomplete
And a movie on YoutTube to show what goes wrong:
https://www.youtube.com/watch?v=ByMsy4AaHYI
Can anyone look at it and spot the bug...?
Thanks in advance!
You've accidentally hidden your cell.
Open Main.storyboard
Select Cell
Uncheck Hidden
Side note: As for why it's displaying the second time around with the cell hidden? It appears to be a bug. It should still be hidden (print cell.hidden, notice it's always true despite showing the text on the screen).
I think you need to change your code. Check out below code. It is because if you remember in Objective C you needed to check if the Cell was nil and then initialise it. The reuse identifier is usually reusing an already created cell, but on the first launch this does not work because there is no Cell to use. Your current code assumes always that the cell is created (re-used) because you are using ! in the declaration, so if you use the optional (?) it can be null and you then can create the cell
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? AutocompleteTableViewCell
if cell == nil
{
//You should replace this with your initialisation of custom cell
cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "CELL")
}
cell.title.text = filteredWords[indexPath.row]
return cell

Resources