Timers aren't updating in UITableView iOS - ios

I am using countdown times in tableView for each cell as shown in the image.
TableView with Countdown timer
If I use the following code for a simple label in the controller, all works fine.
var duration = 0.0
func clock(){
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self](Timer) in
guard let strongSelf = self else { return }
strongSelf.duration -= 0.01
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.hour,.minute,.second]
formatter.zeroFormattingBehavior = .pad
strongSelf.timerLabel.text = formatter.string(from: strongSelf.duration)
})
}
But if I write similar for tableViewCell, this only shows values, but not updating them.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:"myTimer", for: indexPath) as? UITableViewCell
let label = cell?.contentView.viewWithTag(13) as? UILabel
milisecondsClock(index: indexPath.row, label: label!)
return cell!
}
func milisecondsClock(index: Int,label: UILabel){
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self](Timer) in
guard let strongSelf = self else { return }
strongSelf.myList[index] -= 0.01
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.hour,.minute,.second]
formatter.zeroFormattingBehavior = .pad
label.text = formatter.string(from: strongSelf.myList[index])
})
}
How do I show the timer in tableview with real-time countdown?

Table cells are not updated because timers created at millisecondsClock have no owner object and are immideately deleted by ARC. You should store your timers in your view controller the same way you store myList. You can find more info about ARC at https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html
You also shouldn't pass UILabel into millisecondsClock method because cells are reused by UITableView. That means that the same cell and UILabel inside that cell may be used several times to render contant at different index paths. In your case that means that several timers may simultaneously update the same label. I think you should use func cellForRow(at indexPath: IndexPath) -> UITableViewCell? inside timer block to get the correct cell to update the time.

Related

how to change button icon only if time in json response is updated?

I have to change a button icon only if the time in response changes. So I created a timer function which hits api every 30 seconds and reloads table. Here is the code:
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var refreshed = false
override func viewDidLoad() {
super.viewDidLoad()
table.dataSource = self
table.delegate = self
self.getPosts()
timer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { (timer) in
self.refreshed = true
self.getPosts()
}
}
//TableView Datasource
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = posts[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "HomeTableViewCell", for: indexPath) as! HomeTableViewCell
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" // Pass any format here you want according to your date response
let convDate = dateFormatter.date(from: data.postUpdatedAt ?? "")
if refreshed == false {
cell.timeBeforeRefreshInterval = convDate
}
else {
cell.timeAfterRefreshInterval = convDate
if cell.timeAfterRefreshInterval == cell.timeBeforeRefreshInterval {
cell.button.setImage(UIImage(named: "1"), for: .normal)
}
else {
cell.button.setImage(UIImage(named: "2"), for: .normal)
cell.timeBeforeRefreshInterval = cell.timeAfterRefreshInterval
refreshed = false
}
}
return cell
}
}
class HomeTableViewCell: UITableViewCell {
var timeBeforeRefreshInterval : Date?
var timeAfterRefreshInterval : Date?
}
First I created variable to store date response in HomeViewController but it will always store the response of last cell created so I added those variables in HomeTableVIewCell. However its still not working. Is there any better logic to compare times of post and update your cell according to that? All I wanna do is compare time, if someone posts something in group, time gets updated and I need to change button icon to show someone has posted something new.

Guidance needed on using CADisplayLink with tableViewCells

I am creating an app that contains lists which will be valid for a certain amount of time. For that I am creating a countdown bar animation on each cell. Each cell has it's own custom duration after which the cell displays "Time's Up". The cell even shows the time in seconds as it ticks down to 0 and then displays "Time's Up" message.
Right above this, there is a countdown bar animation. I am controlling all these animations from the viewController and not doing these in the customView cell as cell reusability makes the timers go haywire.
I am using two timers:
Timer() : that invokes every 1 second. This is used for the simple seconds countdown. I am using an array of type struct that houses two variables durationTime & width. The width is set to 400 by default. The durationTime is set by the user.
#objc func handleCountdown() {
let arrayLength = duration.count
var i: Int = 0
var timeCount: Int = 0
for _ in 0..<arrayLength {
let value = duration[i].durationTime - 1 //Decreasing the timer every second
let indexPath = IndexPath(row: i, section: 0)
if (value > 0)
{
duration[i].durationTime = value
if let cell = listTableView.cellForRow(at: indexPath) as! listTableViewCell? {
cell.testlabel.text = "\(value)"
}
//listTableView.reloadRows(at: [indexPath], with: .automatic)
i = i + 1
} else {
if let cell = listTableView.cellForRow(at: indexPath) as! listTableViewCell? {
cell.testlabel.text = "Time's Up"
}
i = i + 1
timeCount = timeCount + 1
}
}
if (timeCount == (arrayLength - 1)) {
self.timer?.invalidate()
}
}
The above code is invoked every second. It decrements the value of the time, and then displays it in the tableview cell. The above code works fine as it should.
CADisplayLink: this timer is used to run the progress bar animation. In this case however, I am calculating the elapsed time and dividing it by the durationTime of each element and calculating a percentage. This percentage is used to update the value of width of each progress bar. (Keep in mind that durationBar is simply duplicate of duration array. In this array however, the durationTime is not being decremented -> Just for testing purposes) Here is the code:
#objc func handleProgressAnimation() {
let currenttime = Date()
let elapsed = currenttime.timeIntervalSince(animationStartDate)
let totalWidth: Double = 400.0
print(elapsed)
let arrayLength = duration.count
var i: Int = 0
for _ in 0..<arrayLength {
let indexPath = IndexPath(row: i, section: 0)
let percentage = (elapsed / Double(durationBar[i].durationTime))
let newWidth = Double(totalWidth - (totalWidth * percentage))
durationBar[i].width = newWidth
if let cell = listTableView.cellForRow(at: indexPath) as! listTableViewCell? {
cell.timeRemainingView.frame.size.width = CGFloat(durationBar[indexPath.row].width)
}
i = i + 1
}
}
Question: After some time, some progress bars just disappear from the cells even though the time has not been completed. It occurs most often once I have done some scrolling on the tableview.
This is willDisplayCell & cellForRowIndexPath:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellid, for: indexPath) as! listTableViewCell
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cell:listTableViewCell = cell as! listTableViewCell
cell.testlabel.text = "\(duration[indexPath.row].durationTime)"
cell.timeRemainingView.frame.size.width = CGFloat(duration[indexPath.row].width)
}
As you can in pictures below, after sometime, some progress bars vanished even though there is still some time left:
What is the issue here, I am controlling all the animations and timers from the viewController and not the cell in order to prevent cell reuseability to become a problem. Help is needed!
The thing is that you update cell.timeRemainingView.frame.size.width with
durationBar[indexPath.row].width for handleProgressAnimation and
duration[indexPath.row].width for willDisplayCell.
And also I would switch to tableView.indexPathsForVisibleRows for updating UI only for visible cells so it wouldn't call .cellForRow for every cell in if-let.

Adding Multiple Countdown Timers to UICollectionView

My goal is to have a unique (hh:mm:ss) countdown timer for each populated uicollectionviewcell within a uicollectionview.
Below is the (abbreviated) code I've been able to compile, but am stuck on getting it to really work in any capacity...any guidance would be appreciated.
class VC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var timer = Timer()
var secondsRemaining: Int?
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! ExampleCollectionViewCell
let expirationDate = Calendar.current.date(byAdding: .hour, value: 24, to: startDate)
secondsRemaining = expirationDate?.seconds(from: startDate)
timer = Timer(timeInterval: 1, target: self, selector: #selector(countdown), userInfo: nil, repeats: true)
return cell
}
#objc func countdown() {
secondsRemaining = secondsRemaining! - 1
if secondsRemaining == 0 {
print("CELL TIME HAS EXPIRED!")
timer.invalidate()
} else {
let hours = secondsRemaining! / 3600
let minutes = (secondsRemaining! % 3600) / 60
let seconds = (secondsRemaining! % 3600) % 60
let countdownLabelString = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
//i need to take this countdownLabelString and set it to the
//countdownLabel.text that is an element of each cell. i tried
//doing all of this in the cell subclass, but no luck...
}
}
}
Collection view cells are not made to update constantly. If you want them to update, you have to tell them to update.
If you want each cell to show a different amount of time remaining, you need a data model that saves a separate expirationDate for each indexPath you display. If your collection view is a simple one-dimensional list of items, like a table view, then you could just use a 1-dimensional array. If your collection view shows a grid of cells you might need a 2 dimensional array of model objects.
Each time you add an item to your data model, include an expirationDate value. (The Date when that item expires.)
Then have your timer call the collection view's reloadData method each time it fires.
In your collectionView(collectionView:cellForItemAt:) method, use the indexPath to figure out which item in your data model you are displaying. Look up the expirationDate for that indexPath, calculate the time remaining from now until your expiration date, and display that information in the cell. (Use the Calendar method dateComponents(_:from:to:) to calculate the number of hours, minutes, and seconds between now and your expiration date.)
EDIT
DO NOT decrement a counter every time your timer fires. Do math on the difference between your expiration date and the current date using dateComponents(_:from:to:)
EDIT #2:
A sample collection view controller that does something like what you want is below:
import UIKit
private let reuseIdentifier = "aCell"
class DatesCollectionVC: UICollectionViewController {
weak var timer: Timer?
var dates: [Date] = []
lazy var componentsFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
return formatter
}()
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...100 {
let interval = Double(arc4random_uniform( 10000)) + 120
dates.append(Date().addingTimeInterval(interval))
}
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.collectionView?.reloadData()
}
}
// MARK: - UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dates.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? MyCell else {
return UICollectionViewCell()
}
let now = Date()
let expireDate = dates[indexPath.row]
guard expireDate > now else {
cell.intervalLabel.text = "00"
return cell
}
let desiredComponents: Set<Calendar.Component> = [.hour, .minute, .second]
let components = Calendar.current.dateComponents(desiredComponents, from: now, to: expireDate)
cell.intervalLabel.text = self.componentsFormatter.string(from: components)
return cell
}
}
The code above looks like this:

Reloading Table View to Create Dynamic Label Changes iOS Swift

I am creating an app that I want to display different strings inside of a label that is within a UITableViewCell. However, I cannot use indexPath.row because the strings are in not particular order.
let one = "This is the first quote"
let two = "This is the second quote"
var numberOfRows = 1
var quoteNumber = 0
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let items = [one, two]
myTableView = tableView
let cell = tableView.dequeueReusableCellWithIdentifier("customcell", forIndexPath: indexPath) as! cellTableViewCell
let cellWidth = cell.frame.width
let cellHeight = cell.frame.height
cell.QuoteLabel.frame = CGRectMake(cellWidth/8, cellHeight/4, cellWidth/1.3, cellHeight/2)
let item = items[quoteNumber]
cell.QuoteLabel.text = item
cell.QuoteLabel.textColor = UIColor.blackColor()
if quoteNumber == 0 {
quoteNumber = 1
}
if numberOfRows == 1 {
numberOfRows = 2
}
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
var timer = NSTimer.scheduledTimerWithTimeInterval(5, target: self, selector: "update", userInfo: nil, repeats: true)
}
func update() {
dispatch_async(dispatch_get_main_queue()) {
self.myTableView.reloadData()
}
This code builds and runs, but when the timer I have set expires and the update function is ran, both of the two available cells change to the same label. What I am trying to do is make it so that the first cell can remain static while the newly added cell can receive the new element from the 'items' array without using indexpath.row (because the quotes that I am going to be using from the items array will be in no particular order). Any help would be appreciated.
Instead of reloadData try using reloadRowsAtIndexPaths: and pass in the rows you want to reload. This does use indexPath, however. The only way I'm aware of to reload specific rows as opposed to the whole tableView is to specify the rows by index path.

NSTimer update in real time?

I have an NSTimer and I want to update a label in real time which shows a timer's time. The following is my implementation of how I came about doing so:
override func viewDidAppear(animated: Bool) {
self.refreshTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "refreshView:", userInfo: nil, repeats: true)
var currentRunLoop = NSRunLoop()
currentRunLoop.addTimer(refreshTimer, forMode: NSRunLoopCommonModes)
}
func refreshView(timer: NSTimer){
for cell in self.tableView.visibleCells(){
var indexPath = self.tableView.indexPathForCell(cell as! UITableViewCell)
self.tableView(self.tableView, cellForRowAtIndexPath: indexPath!)
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return offerCellAtIndexPath(indexPath)
}
func offerCellAtIndexPath(indexPath: NSIndexPath) -> OfferCell{
//Dequeue a "reusable" cell
let cell = tableView.dequeueReusableCellWithIdentifier(offerCellIdentifier) as! OfferCell
setCellContents(cell, indexPath: indexPath)
return cell
}
func setCellContents(cell:OfferCell, indexPath: NSIndexPath!){
let item = self.offers[indexPath.row]
var expirDate: NSTimeInterval = item.dateExpired()!.doubleValue
var expirDateServer = NSDate(timeIntervalSince1970: expirDate)
//Get current time and subtract it by the predicted expiration date of the cell. Subtract them to get the countdown timer.
var timer = self.modelStore[indexPath.row] as! Timer
var timeUntilEnd = timer.endDate.timeIntervalSince1970 - NSDate().timeIntervalSince1970
if timeUntilEnd <= 0 {
cell.timeLeft.text = "Finished."
}
else{
//Display the time left
var seconds = timeUntilEnd % 60
var minutes = (timeUntilEnd / 60) % 60
var hours = timeUntilEnd / 3600
cell.timeLeft.text = NSString(format: "%dh %dm %ds", Int(hours), Int(minutes), Int(seconds)) as String
}
}
As seen by the code, I try to do the following:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.timeLeft.text = NSString(format: "%dh %dm %ds", Int(hours), Int(minutes), Int(seconds)) as String
})
Which I thought would help me update the cell in real time, however when I view the cells, they are not being updated in real time. When I print out the seconds, minutes, and hours, they are being updated every 1 second which is correct. However, the label is just not updating. Any ideas on how to do this?
EDIT: DuncanC's answer has helped a bunch. I want to also delete the timers when their timers go to 0. However, I am getting an error saying that there is an inconsistency when you delete and insert most likely extremely quickly without giving the table view any time to load. Here is my implmentation:
if timeUntilEnd <= 0 {
//Countdown is done for timer so delete it.
self.offers.removeAtIndex(indexPath!.row)
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
cell.timeLeft.text = "Finished."
}
Your problem isn't with timers. Your problem is that you are handling table views totally wrong. In addition to the problems pointed out by Matt in his answer, your timer method refreshView makes no sense.
func refreshView(timer: NSTimer)
{
for cell in self.tableView.visibleCells()
{
var indexPath = self.tableView.indexPathForCell(cell as! UITableViewCell)
self.tableView(self.tableView, cellForRowAtIndexPath: indexPath!)
}
}
You are looping through the visible cells, asking for the indexPath of each cell, and then asking the table view to give you that cell again. What do you think this will accomplish? (The answer is "nothing useful".)
What you are supposed to do with table views, if you want a cell to update, is to change the data behind the cell (the model) and then tell the table view to update that cell/those cells. You can either tell the whole table view to reload by calling it's reloadData method, or use the 'reloadRowsAtIndexPaths:withRowAnimation:' method to reload only the cells who's data have changed.
After you tell a table view to reload certain indexPaths, it calls your cellForRowAtIndexPath method for the cells that need to be redisplayed. You should just respond to that call and build a cell containing the updated data.
This is madness:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return offerCellAtIndexPath(indexPath)
}
func offerCellAtIndexPath(indexPath: NSIndexPath) -> OfferCell{
//Dequeue a "reusable" cell
let cell = tableView.dequeueReusableCellWithIdentifier(offerCellIdentifier) as! OfferCell
setCellContents(cell, indexPath: indexPath)
return cell
}
Your job in cellForRowAtIndexPath: is to return the cell - now. But setCellContents does not return any cell to offerCellAtIndexPath, so the cell being returned is merely the empty OfferCell from the first line. Moreover, setCellContents cannot return any cell, because it contains an async, which will not run until after it has returned.
You need to start by getting off this triple call architecture and returning the actual cell, now, in cellForRowAtIndexPath:. Then you can worry about the timed updates as a completely separate problem.
You could use a dispatch timer:
class func createDispatchTimer(
interval: UInt64,
leeway: UInt64,
queue: dispatch_queue_t,
block: dispatch_block_t) -> dispatch_source_t?
{
var timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
if timer != nil
{
dispatch_source_set_timer(timer, dispatch_walltime(nil, 0), interval, leeway)
dispatch_source_set_event_handler(timer, block)
return timer!
}
return nil
}

Resources