timer.gif
↑↑↑↑↑↑↑↑↑ I show the problem in this GIF ↑↑↑↑↑
There is a timer and stratButton in the tableView cell, when I click the Button, the timeLabel will start running.
Now the problem is when I scroll the running timer out of the screen and scroll it back, the timer will reset and stop.
Hope someone can help me! I check many solution but they didn't work for me! I am almost cry.
my cellForRow:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! TimerTableViewCell
cell.timer?.invalidate()
let item = myTimerList[indexPath.row]
cell.timerName.text = item.timerName
cell.secondLeftLabel.text = "\(item.timerSecond)"
return cell
}
I created a small project contained my code for you to modify : https://app.box.com/s/axluqkjg0f7zjyigdmx1lau0c9m3ka47
You need to preserve the state of each cell
class Service {
static let shared = Service()
var myTimerList = [TimerClass]()
}
//
here i added another 2 vars , why timerName is left despite you have to init them the same because current will hold the changing value
class TimerClass {
let timerSecond : Int
let timerName : String
var current: Int
var isPlaying = false
init(second:Int, name:String) {
timerSecond = second
timerName = name
current = second
}
}
//
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! TimerTableViewCell
let item = Service.shared.myTimerList[indexPath.row]
cell.tag = indexPath.row // to access the array inside the cell
if item.isPlaying {
cell.play() // add play method to the cell it has same button play action
}
else {
cell.timer?.invalidate()
}
cell.timerName.text = item.timerName
cell.secondLeftLabel.text = "\(item.current)"
return cell
}
//
inside the cell when the button is clicked , change isPlaying property to true and to false when stopped like this
// here self is the cell itself
Service.shared.myTimerList[self.tag].isPlaying = true
also when the timer ticks change
Service.shared.myTimerList[self.tag].current = // value
You are reusing UITableViewCell. It means once cell is out of screen, it will be reused for the cell getting into the screen. Whenever it happens, it calls the delegate method - means cell.timer?.invalidate() is invoked again and again.
Remove cell.timer?.invalidate() from cellForRowAt method, because every time you scroll the tableView this method is called to display new cells. In your case when you have started a timer for a cell and that cell is not in the view, next time you scroll to bring that cell again to the view cell.timer?.invalidate() causes it to stop.
Related
in my View:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TransactionTableCell", for: indexPath) as! TransactionTableCell
let newItem = getTransactionsInSection(section: sectionHeader[indexPath.section])[indexPath.row]
cell.configure(item: newItem)
}
in my TransactionTableCell
func configure(item: TransactionModel) {
guard let withdrawalBonuses = item.withdrawalBonuses,
withdrawalBonuses < 0,
let accruedBonuses = item.accruedBonuses,
accruedBonuses > 0 else {
configureWithOneOperation(item)//shows one line of operation
return
}
//show 2 lines of operations
firstOperationAmountLabel.text = "+\(Int(accruedBonuses))"
secondOperationAmountLabel.text = "\(Int(withdrawalBonuses))"
}
When I scroll the cell , second operation line is appears in wrong cells where its shouldn't be, even If I reload my table , that also has this problem.
You should use prepareForReuse() method
Simply just clear data of your labels:
override func prepareForReuse() {
super.prepareForReuse()
firstOperationAmountLabel.text = nil
secondOperationAmountLabel.text = nil
}
There are few things to check here.
Make sure you reset all fields before configure a new cell.
If you have created a cell using xib or storyboard, make sure you haven't filled labels with static text.
Is your guard statements passing for every item?
Else block for guard configures cell with a single operation, Is it handling all ui elements in cell?
I have an table view with 3 cells. In one of the cell I am trying to display a timer which apparently messes up my cells contents when scrolling.
Here is my code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! StatsuSessionTableViewCell
if indexPath.row == 0 {
// do something
return cell
} else if indexPath.row == 1 {
// do something
return cell
} else if indexPath.row == 2 {
_ = Timer.every(1.second) { (timer: Timer) in
let time = self.date.timeIntervalSince(Date())
cell.statusTitle.text = "TIME"
cell.statusDescription.text = time.timerFormat
if time < 0 {
timer.invalidate()
}
}
return cell
}
return cell
}
If I remove the timer and just display some text, I don't have any issue. But apparently because cell's are destroyed and recreated this messes up my contents, the timer would also appear at indexpath.row 0 not just indepxath.row 2.
Is there any workaround for this ?
So your problem is that you are creating the timer in the cellForRowAt:. The timer also works for the cell that was created by reusing this cell. So you have to implement this timer logic in the StatsuSessionTableViewCell. Also implement prepareForReuse method in the cell and invalidate the timer there. This is the safest way that I can suggest.
Good time of day, I'm new in iOS development. I'm developing project where i have tableViewCell and button,progressBar on it. When i tap button indexPath of this cell is passed through delegate to viewController and then with another method i'm
downloading some data and show it's progress in progressBar. So when i tap to one cell and then to another, progress in first cell stops and continues in second, could anyone help? Thanks in advance )
Here is delegate methods in viewController:
func didTouchButtonAt(_ indexPath: IndexPath) {
songs[indexPath.row].isTapped = true
let selectedSong = self.songs[indexPath.row] as Song
DownloadManager.shared.delegate = self
self.indexQueue.append(indexPath)
self.selectedIndexPath = indexPath
DownloadManager.shared.download(url: selectedSong.url , title: selectedSong.title)
}
func downloadProgress(_ progress: Progress) {
if (progress.completedUnitCount) < progress.totalUnitCount {
selectedIndexPath = indexQueue.first
}
else if(!indexQueue.isEmpty){
indexQueue.removeFirst()
}
print(progress.fractionCompleted)
print(progress.completedUnitCount, progress.totalUnitCount)
print(indexQueue.count)
var cell: ViewControllerTableViewCell?
cell = self.tableView.cellForRow(at: self.selectedIndexPath!) as? ViewControllerTableViewCell
if cell != nil {
cell?.progress = Float(progress.fractionCompleted)
}
}
this is cell:
#IBAction func downloadButtonTouched(sender: Any){
self.delegate?.didTouchButtonAt(self.indexPath!)
self.progressBar.isHidden = false
}
As #RakshithNandish mentioned, I'v used list of indexPathes and when i tap to button, list adds indexPath. So, before passing progress to cell i check if progress is completed: if not, pass progress to first element of queue, otherwise just delete first element from queue, works fine.
You can create a model that may be an array which will hold the indexpath of tapped button's cell i.e. append indexpath to the array whenever button is tapped and remove it whenever you want. Later on while returning cells in cellForRowAtIndexPath you check if the array contains indexPath for which you are returning cell.
class DemoCell: UITableViewCell {
#IBOutlet button: UIButton!
}
class DemoTableViewController: UITableViewController {
var buttonTappedIndexPaths: [IndexPath] = []
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: DemoCell.className, for: indexPath) as! DemoCell
if buttonTappedIndexPaths.contains(indexPath) {
//show progress view spinning or whatever you want
} else {
//don't show progress view
}
}
}
I have a tableview and in each cell there is a checkbox. I also have a "select all" button.
My problem is that when I click select all I want to update all the checkboxes to checked state. So from a list of 100 cells, all get checked but every 13th cell does not. To make it clearer, on my simulators screen are 12 cells visible that all get checked. When I start scrolling, the first cell that comes up is unchecked, and is then followed by 12 checked ones :S
When I scroll a little and click "select all" again, the skipped ones become also checked..
Anyone have a clue what am I missing?
This is the cell code:
class ListTableViewCell: UITableViewCell {
#IBOutlet weak var checkbox: UIButton!
var buttonState = false{
didSet{
if buttonState{
checkbox.setImage(#imageLiteral(resourceName: "checked"), for: .normal)
}else{
checkbox.setImage(#imageLiteral(resourceName: "unchecked"), for: .normal)
}
}
}
#IBAction func checkboxAction(_ sender: UIButton) {
if buttonState {
buttonState = false
}else{
buttonState = true
}
}
func simulateCheck(){
buttonState = true
}
And here are some snipets from my controller:
private var articleValues: [ArticleValue] = []{
didSet{
tableView.reloadData()
}
}
func selectAll(){
for i in 0..<articleValues.count{
let cell = tableView.cellForRow(at: IndexPath(item: i, section: 0)) as? ListTableViewCell
cell?.simulateCheck()
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
}
return cell
}
Your UITableView is backed by a data source. This means that you shouldn't change cells directly like you do here:
cell?.simulateCheck()
tableView.reloadData()
Instead you should keep a list of all the checked positions, maybe another array that has bools for each corresponding articleValue (this is not the best design).
var checkedValues = Bool
In your
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method you would then set the state of the cell:
articleValueCell.buttonState = checkedValues[indexPath.row]
In your selectAll method fill this array with true values and then call tableView.reloadData()
private var checkedValues = [Bool]()
private var articleValues: [ArticleValue] = []{
didSet{
checkedValues = Array(repeating: false, count: articleValues.count)
tableView.reloadData()
}
}
func selectAll(){
checkedValues = Array(repeating: true, count: articleValues.count)
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
articleValueCell.buttonState = checkedValues[indexPath.row]
}
return cell
}
Another mistake is that you should never iterate on all the cells in the table because they are reused, no point in going through your data source and getting a cell for each. It only makes sense to iterate through tableView.visibleCells. But like in your case, most of the time you don't need that either, you should just update your data source accordingly and reload the table or just the modified cell.
It's not recommended that you refer to cells directly within a table view. The reason is that UITableViews have an efficient method of only loading the cells as they are needed (and deallocating them when they are no longer needed, e.g. the cell scrolls off screen). Because of this the cell you are try to refer to may not be loaded.
Instead you should interact with it via the cellForRowAt method. If you want to "select all" cells, you should create a property that stores the value of checked or not checked via a Bool and then set all of the ArticleValue elements to true for that property and reload the data inside selectAll().
It could work something like this:
func selectAll() {
articleValues.forEach {
$0.checked = true
}
tableView.reloadData()
}
// ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
if articleValue.checked {
articleValueCell.simulateCheck()
}
}
return cell
}
every time I scroll up or down value get changed with wrong value at 1st it shows correct value but after scrolling it get changed every time.
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
//creating a cell using the custom class
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ShowPercentageAttandanceTableViewCell
cell.rollNo.text = String(describing: self.mainArrayRoll[indexPath.row])
var location = Int()
for item in self.rollPercentage {
if item.rollCall == indexPath.row {
location = item.absentCount
cell.percentage.text=String(location)
}
else if cell.percentage.text==("Label")
{
cell.percentage.text=String("0")
}
}
return cell
}
Here is the code.
[![Here in image 2 label is having text =2 and gray color label is having text 0 but it automatically get change after scroll as shown in image 2[![image 2 label changes after scroll but it is showing wrong][2]][2]
You are failing to set the cell.percentage.text to "0" when you didn't find the value you were looking for and you had a reused cell.
Try this:
if let item = self.rollPercentage.first(where: { $0.rollCall == indexPath.row }) {
location = item.absentCount
cell.percentage.text = String(location)
} else {
cell.percentage.text = "0"
}