I have a table view with ten cells. Each of them contains an image view. When scrolling, sometimes images start jumping from one cell to another - I guess reuse does not work. I tried to reset the image to nil in prepareForReuse(), but the problem persists.
catImageView.image = nil
I also reset indexPath in cellForRowAt, but that didn't work either.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchCell.identifier, for: indexPath) as? SearchCell else { return UITableViewCell() }
let cat = cats[indexPath.row]
cell.setup(cat)
cell.catImageView.image = nil
return cell
}
In rxSwift for this I cleaned disposedBag. What can be done in this case?
Related
I have a tableview in my storyboard where the prototype cell has a disclosure indicator by default.
When I populate my table I want to remove the indicator only from the last cell AND center a spinner on it.
I'm doing it like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CharacterCell", for: indexPath) as! CharacterCell
if indexPath.row == charactersViewModel.charactersCount - 1 {
cell.accessoryType = .none
cell.accessoryView = .none
// Spinner
let spinner = UIActivityIndicatorView(style: .large)
spinner.color = .white
spinner.center = cell.contentView.center
cell.contentView.addSubview(spinner)
spinner.startAnimating()
}
return cell
}
The problem is that the spinner is offcenter, a little bit to the left, just like if the accessory is still there, but hidden.
I feel maybe I'm missing the lifecycle of a table cell, maybe it's getting the center value of the content view when the accessory is still there, so when it's removed it is offcenter?
I tried on willDisplay as well but the same thing happens.
Any tips on this?
As #Paulw11 mentioned, I used a second subclass and created another cell prototype in my tableview.
Then when the last position at the table is reached, we can use the second prototype on cellForRowAt.
Here how it is:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row >= charactersViewModel.charactersCount - 1 {
reloadRows(indexPath: indexPath)
let cell = tableView.dequeueReusableCell(withIdentifier: "LoadingCharacterCell", for: indexPath) as! LoadingCharacterCell
cell.startSpinner()
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "CharacterCell", for: indexPath) as! CharacterCell
cell.configureCell(charactersViewModel: charactersViewModel, cell: cell, index: indexPath.row)
return cell
}
}
private func reloadRows(indexPath: IndexPath) {
var indexPathList = [IndexPath]()
indexPathList.append(indexPath)
charactersTableView.reloadRows(at: indexPathList, with: .automatic)
}
And with the reloadRows function, the last cell is updated and removed when the table receives more data.
I am currently working on a swift based HRM project. where it requires to show a tableview with slightly customized cell. cells it self containing two buttons, under some business logic one button would be hidden. for example ,
if the current user is the employee himself , he can see a list, the cell containing his name can see two buttons,but other cell would show simply one button.
i have tried the followings:
1. if the userId == employeeId (employeeId came from a model) then ,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ClaimTableViewCell", for: indexPath) as! ClaimTableViewCell
if(self.claimdata[indexPath.section].employeeId == self.empId) {
cell.CancelButton.isHidden = false
}
also , i have tried
if(self.claimdata[indexPath.section].employeeId != self.empId) {
cell.CancelButton.frame.size.height = 0
}
works fine for the first frame , the problem begins when i begin to scroll. for some unintended cell it also shows two buttons.
Am I missing something?
This issue is due to the cell reusability in UITableView.
Use below code in your cellForRowAtIndexPath method.
cell.CancelButton.isHidden = true
if(self.claimdata[indexPath.section].employeeId == self.empId) {
cell.CancelButton.isHidden = false
}
As tableView cell is reusableCell
dequeueReusableCell withIdentifier
you just need to give else condition so when it reuse the cell again it knowns what to do with CancelButton.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ClaimTableViewCell", for: indexPath) as! ClaimTableViewCell
if(self.claimdata[indexPath.section].employeeId == self.empId) {
cell.CancelButton.isHidden = false
}else{
cell.CancelButton.isHidden = true
}
}
This is a part of my storyboard:
this is my running app:
This is my part of codes:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
if indexPath.row == 0 {
return super.tableView(tableView, cellForRowAt: indexPath)
} else {
tableView.register(SubTextFieldCell.self, forCellReuseIdentifier: "SubTextFieldCell")
let cell = tableView.dequeueReusableCell(withIdentifier: "SubTextFieldCell", for: indexPath) as! SubTextFieldCell
// cell.deleteButton.isEnabled = true
// cell.subTextfield.text = "OK"
print("indexPath.row: \(indexPath.row)")
return cell
}
...
I have already connected the button and the textfield in various places and I can guarantee that this part is not wrong, but when I click the Add button in the first row, I only get a cell without any content.
If I use code like this cell.deleteButton..., Xcode will report an error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an
Optional value
Then I tried to use the viewWithTag method to see if show the content, but I still get the same error as before.
This is the first time I have encountered this kind of error. I have no error with similar code and methods in my other programs.
When you configure custom cells inside a storyboard file, you don't need to call register(_:forCellReuseIdentifier:) because the storyboard should have done that for you.
The reason deleteButton is nil is because by re-registering the cell class as you did, you overwrote what the storyboard registered for you. All cells created by dequeueing with that reuse identifier will have no connection to the storyboard and simply be empty.
Assuming all the #IBOutlets and reuse identifiers and things are set up (which you said you did), then simply dequeue the cell with the reuse identifier set up in storyboard.
Dequeue Cell Example:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
if indexPath.row == 0 {
return super.tableView(tableView, cellForRowAt: indexPath)
} else {
// Registering again is unnecessary, because the storyboard should have already done that.
// tableView.register(SubTextFieldCell.self, forCellReuseIdentifier: "SubTextFieldCell")
let cell = tableView.dequeueReusableCell(withIdentifier: "SubTextFieldCell") as! SubTextFieldCell
cell.deleteButton.isEnabled = true
cell.subTextfield.text = "OK"
return cell
}
} else {
...
}
}
Note:
Even in cases where you do need to register a class with a table view, you should only have to do this once. (For example, during viewDidLoad)
Even in those times, you should not call it every time you dequeue a cell. You're just making your app work harder.
Connecting views to cells in Storyboard
Set a subclass to the table view
Set a subclass to the first prototype cell
Set a reuse identifier to the prototype cell
Make sure subview (UIButton, etc.) is connected to property with #IBOutlet (subclass code shown below)
Example UITableViewController subclass:
class MyTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyFirstCell", for: indexPath) as! MyFirstTableViewCell
// Configure cell if needed
cell.myButton.setTitle("New Button Text", for: .normal)
cell.myButton.tintColor = .green
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "MySecondCell", for: indexPath) as! MySecondTableViewCell
// Configure cell if needed
cell.myTextField.backgroundColor = .red
return cell
}
}
}
Example UITableViewCell subclass:
class MyFirstTableViewCell: UITableViewCell {
#IBOutlet weak var myButton: UIButton!
}
Result:
I know there are a lot of questions regarding this topic but bear with me cause I didn't find any solution to my specific problem.
First of all, my entire app is in code (no storyboard). I am creating a tableView (where I get data from a backend) with multiple cell types. The cells are in constant order meaning that the first cell will always be the same type as any other first cell (not sure if you get my point but technically the cell types are constant to a specific indexPath.
So the problem is I have methods that dequeues the specific cell for each indexPath. In one of my cells, I have a MapboxStatic map, which returns an image of a map from Mapbox's servers. Let's say the cell is off the screen at initial run time, if I scroll fast enough, the tableView will hold off for a second or so to load the image returned (I tried loading it asynchronously on the main queue still with no changes).
Now my other problem is with the UberRides button, it also lags when scrolling (same issue).
It's worth mentioning that if I scroll through the tableView once (with the lag), and I scroll up and back down it does not lag anymore but I guess it's because the dequeued cell is still in memory.
If you need any other details please leave a comment.
[EDIT] Code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 1 {
return reserveTableViewCell(at: indexPath)
} else if indexPath.section == 2 {
return dateTimeTableViewCell(at: indexPath)
} else if indexPath.section == 3 {
return descriptionTableViewCell(at: indexPath)
} else if indexPath.section == 4 {
if indexPath.row == 0 {
return mapTableViewCell(at: indexPath)
} else {
return uberTableViewCell(at: indexPath)
}
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: defaultCell, for: indexPath)
return cell
}
}
private func reserveTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reserveCell, for: indexPath) as! ReserveTableViewCell
return cell
}
private func descriptionTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: descriptionCell, for: indexPath) as! DescriptionTableViewCell
return cell
}
private func dateTimeTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: dateTimeCell, for: indexPath) as! DateTimeTableViewCell
return cell
}
private func mapTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: mapCell) as! MapTableViewCell
cell.detailController = self
return cell
}
private func uberTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: uberCell, for: indexPath) as! UberTableViewCell
return cell
}
Best
Well, creating your cells is slow. It's hard to give you any suggestions now knowing what takes so much time. Use time profiler to determine the problematic code.
If you do not need overlays, you could try to use MGLMapSnapshotter. Here is an example of how a MGLMapSnapshot can be used with a table view.
I want to have ActivityIndicatorView inside each UITableViewCell and start animation after tap (to show that action is in progress). But I cannot start the animation effect.
I added ActivityIndicatorView to my cell prototype and connected it via #IBOutlet. This is my code to start animation after user selects table row:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedAction = actions[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath) as! ActionTableViewCell
cell.activityInProgressIndicator.startAnimating()
}
I also tried adding new instance of ActivityIndicatorView as cell.accessoryView. Without any effect.
I also tried to update the cell either via tableView.reloadData() (which I would like to avoid) and tableView.beginUpdates() and tableView.endUpdates()
Ideally this spinning indicator should be hidden but setting .isHidden = false in didSelectRowAt also does not work.
Just replace
let cell = tableView.dequeueReusableCell(withIdentifier: "actionCell",
for: indexPath) as! ActionTableViewCell with below line
let cell = tableView.cellForRow(at: indexPath) as! ActionTableViewCell.
Hope it will work!!
The problem is that you're creating new instance of ActionTableViewCell when you use tableView.dequeueReusableCell inside didSelectRowAt, which is wrong.
You need to use something like this:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ActionTableViewCell else { return }
cell.activityInProgressIndicator.startAnimating()
}
This will fix your issue.