NSTimer update in real time? - ios

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
}

Related

Multiple Timers in Tableview Swift

I am trying to have timer for each row which is added manually when user clicks on add button.
Start time is set to 100units(not important for the question) and it should count down.
When new row is added it will have it's own timer started and display the value on this new row.
I tried to have timer in each cell but this is creating issue when dequeuing, so I created array of timer to hold corresponding timer for each cell. Problem I am facing right now is how to update the cell value every second(timer interval)
MultipleTimersTableView_Gist is the link for the code I wrote so far. I thought of using delegate to update the cell but not sure with the best approach.
Any help would be appreciated.
EDIT:
Timers should show the time in increasing order because each row is created(along with timer)from top to bottom meaning first will have less time than next. Looks like while dequeueing something messed up.
Here is the gist I used for above screenshot
Here is how you can handle timers in each UITableViewCell.
Create a custom UITableViewCell
class CustomCell: UITableViewCell {
//MARK: Outlets
#IBOutlet weak var label: UILabel!
//MARK: Internal Properties
var handler: ((Int)->())?
//MARK: Private Properties
private var timer: Timer?
private var counter = 100 {
didSet {
DispatchQueue.main.async {
self.label.text = "\(self.counter)"
self.handler?(self.counter)
}
}
}
//MARK: Internal Methods
func configure(with counter: Int) {
self.counter = counter
self.setTimer()
}
//MARK: Private Methods
private func setTimer() {
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {[weak self] (timer) in
if let counter = self?.counter, counter > 0 {
self?.counter -= 1
} else {
timer.invalidate()
}
})
}
}
In the above code,
I've created a label that will update the counter value in UI.
handler - it will and store the updated counter value somewhere (in ViewController, explained further) when the cell is moved out of the screen
timer - schedule the timer in the cell with timeinterval = 1
counter - current counter value for each cell
In the ViewController,
class VC: UIViewController, UITableViewDataSource {
let numberOfCells = 20
var timerArr = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
self.timerArr = [Int](repeating: 100, count: numberOfCells)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.numberOfCells
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
cell.configure(with: self.timerArr[indexPath.row])
cell.handler = {[weak self] (counter) in
self?.timerArr[indexPath.row] = counter
}
return cell
}
}
In the above code,
timerArr - keeps track of the counter value for each cell in the tableView.
In tableView(_:cellForRowAt:), the counter for each cell is updated using the handler we created previously in CustomCell.
I would use one timer for all the cells. When creating your object and adding it to the datasource, record the dateAdded = Date(), then when the cells are rebuilt on the timer fire, get the seconds count for each cell from the dateAdded field and update the cell in cellForRowAtIndex.

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.

UITableViewCell contents change when scrolling because running a timer in one of the 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.

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.

How to update tableView cells?

I have a tableView and I want update it every 10 seconds. For it I do:
var timer = NSTimer.scheduledTimerWithTimeInterval(10, target: self, selector: "reloadTable", userInfo: nil, repeats: true)
func reloadTable() {
print("reload")
self.tableView.reloadData()
}
but it doesn't reload correctly. What it means:
Yes, it reloads, but my data doesn't update, but when I drag my tableView to top and leave it, my tableView cells' data update. How can I achieve this effect programmatically?
UPDATE
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> onlineUserCell {
let cell:onlineUserCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! onlineUserCell
let user = OneRoster.userFromRosterAtIndexPath(indexPath: indexPath)
cell.username.text = user.displayName
if user.unreadMessages.intValue > 0 {
cell.backgroundColor = .orangeColor()
} else {
cell.backgroundColor = .whiteColor()
}
configurePhotoForCell(cell, user: user)
cell.avatarImage.layer.cornerRadius = cell.avatarImage.frame.size.height / 2
cell.avatarImage.clipsToBounds = true
return cell;
}
I'm not 100% sure, but if the table isn't visually updating, you probably aren't calling the UI code on the main thread. All UI code needs to be executed on the main thread in iOS.
Look up dispatch_async and/or performSelectorOnMainThread.
You need to update your datasource programmatically too.The reloadData() method just refresh the cell.If you don't refresh the datasource(the actually array) there is noting different.

Resources