Timer on table view Cell - swift

there are 5 cells in table view, each have countdown on it, but countdown running fast, it decreases more than 1 second in one call.
this is my table view cell class, I have created a labelSatus here
class ActivityCell: UITableViewCell {
#IBOutlet weak var labelStatus: UILabel!
//Variable Declaration
var timer: Timer?
var totalTime:Double = 0
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func designCell(data:ActivityModal, removeTimer:Bool){
if removeTimer {
if let timer = self.timer {
timer.invalidate()
self.timer = nil
}
}
else{
let timeStampLimit = (data.date)/1000 + 86400 //for 24 hours
let currentTimeStamp = NSDate().timeIntervalSince1970
if timeStampLimit > currentTimeStamp{
self.totalTime = timeStampLimit - currentTimeStamp
self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
}
}
#objc func updateTimer() {
self.labelStatus.text = String.getString(CommonUtils.convertTimeStampToHour(unixtimeInterval: self.totalTime, dateFormat:"HH:mm:ss"))
if self.totalTime != 0 {
self.totalTime -= 1 // decrease counter timer
} else {
if let timer = self.timer {
timer.invalidate()
self.timer = nil
}
}
}
}

There are a few issues to take care of when working with timers on table view cells. Note that the UITableView reuses the cell objects, so after a cell disappears from the screen after scrolling, it will be reused to be displayed in another place inside the table view.
Supposing you call designCell() in tableView:(_:cellForRowAt:) method, you might be creating more timers for a single cell (i.e. a lot of timers which will trigger the same cell's updateTimer() method). Note that a Timer will not be deallocated after you drop all your references to it, if the timer is still valid. At this point, you might be creating a new timer for a specific cell without having the chance to invalidate the old one.
This would be handled by stopping the timer in preapreForReuse() method from the table view cell subclass. This method is called when the cell is going to be reused:
class ActivityCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
self.timer?.invalidate()
self.timer = nil
}
}
This will solve (one of) the timer problem(s), but there is one more issue: you are going to lose the time tracked by that cell. There is no way to preserve that state into the cell itself. You must do it in the view controller and pass the displayed cell the time it needs to track in tableView(_:cellForRowAt:) method. If you are already doing it, then this is not a problem.
Another problem is that the timers will live forever (or until it's cell stops it). For some of the timers, if the cell object lost the reference to it, nobody will ever stop that timer anymore, so it will just fire every second until the app is killed.
I'm not really sure at this point whether the timer will retain the cell object using the target-selector method for creating a timer, but there is an issue regardless of that:
If the timer retains the cell object, that means that you will have memory leaks: that means that the timers and the cells will never be deallocated and will continuously use memory and processing resources;
If the timer does not retain the cell object, then there might occur a crash once the cell is being deallocated.
You should stop the cell's timer in tableView(_:didEndDisplaying:forRowAt) method. At this point, the cell is being hidden, so the timer makes no sense anymore.
Of course, all of this will lead you to another issue: preserving the time state for the cells. This handling should be done from the view controller presenting the table view, and not from the table view cell.
TL;DR:
You will have a lot of headaches having a timer on each cell, in fact it's almost impossible to handle things that way. You should have your timers corresponding to each cell in the view controller that presents the table view and the things will simplify a lot.

Related

kill and re-run background timer (DispatchSourceTimer)

In a game screen, I use a background queue to count elapsed time in a game, this func is based on Daniel Galasko solution, a perfect solution for my app: it allows user to navigate through other VC, while timer is still on.
The VC hierarchy is quite simple : the game settings VCs are handled in a tabBarController. The game screen is apart. User can change settings while timer is on. Settings are stored in CoreData.
In my game screen, where I need to display the timer, I have a label that displays the elapsed time and 2 buttons : Play/Pause button and Reset button.
I call my setup timer func in ViewDidLoad.
The default value for my timer is the stored value in CoreData, it has been defined in settings. And this value is incremented by 1 every second when timer is on.
I also have a static let shared that keep timer status (resumed / suspended).
When I'm on the game screen, and if my timer is suspended, my play/Pause button works perfect : I can navigate to others views (mean dismiss my screen game), present again my screen game and resume my counter. It updates my label correctly.
The problem is when I dismiss the game screen view while timer is running. Timer works (print func shows that timer is still running), but when I present the screen again, I'm unable to pause/resume/restart it, and my label is stopped at the second I came back... while timer is still running back.
private var counter: Int16?
var t = RepeatingTimer(timeInterval: 1)
let gameIsOn = isGameOnManager.shared
override func viewDidLoad() {
super.viewDidLoad()
print("is timer On ? \(String(describing: gameIsOn.isgameOn))")
buildTimer()
if gameIsOn.isgameOn == true {
resumeTapped = true
t.resume()
PlayB.setImage(UIImage(named:"pause"), for: .normal)
} else {
resumeTapped = false
}
}
func buildTimer(){
self.t.eventHandler = {
self.counter! += 1
print("counter \(String(describing: self.counter!))")
self.coreDataEntity?.TimeAttribute = self.counter ?? 0
self.save()
DispatchQueue.main.async {
self.dataField.text = String(describing: self.counter ?? 0)
}
}
}
#objc func didTapButton(_ button: UIButton) {
if resumeTapped == false {
t.resume()
resumeTapped = true
gameIsOn.isgameOn = true
PlayB.setImage(UIImage(named:"pause"), for: .normal)
}
else if resumeTapped == true {
t.suspend()
resumeTapped = false
gameIsOn.isgameOn = false
PlayB.setImage(UIImage(named:"play"), for: .normal)
}
}
You have a strong reference cycle between your view controller, the timer, and the timer’s event handler. You should use a weak reference in the closure to break this cycle:
func buildTimer() {
t.eventHandler = { [weak self] in
guard let self = self else { return }
...
}
}
That fixes the strong reference cycle.
But when you fix this, there is a chance that you’re going to see your timer stop running when you dismiss this view controller. If this is the case, that will indicate that the timer is not in the right object. It should be in some higher level object that persists throughout the app, not inside this view controller which is presented and dismissed.

Variables are not updated in timer completion block

Here is the scenario:
I want to scroll(or shake) a horizontal collectionView after a page showing up so user could see it scrolling...
I do it with no problem 3 sec after page pops up but I don't want it to do the dance when user reach the collectionView before 2 sec & scrolls it itself.
So here is my solution for it:
var collectionDidScroll = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionDidScroll = true
}
override func awakeFromNib() {
super.awakeFromNib()
Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(showScrollTutorial), userInfo: nil, repeats: false)
}
#objc func showScrollTutorial() {
if !collectionDidScroll {
collectionView.shakeCells()
}
}
The Problem:
The "collectionDidScroll" is update just fine in class, but in timer completion its always false!
It means "collectionDidScroll" is not updating in completion & it has its launch time value.
Notice:
My class is a subclass of UICollectionViewCell
I even tried dispatchQueue with timer & timer with completion block but the results are the same
The comments above answered your question, but to help you tackle similar problems in the future yourself:
Whenever you have problems like this, you should try littering your code (especially scrollViewDidScroll and showScrollTutorial) with print-statements to see if they get called at all and which values they contain. Or you can set breakpoints!
Problem Solved!
I placed 'showScrollTutorial()' function contents inside a 'DispatchQueue' to force it to run in main thread, now it works like a charm!
Thanks for everyone.

Swift RunLoop blocks table view

So, I have a timer which increments a variable by one in my app delegate every 0.1 seconds. In my tableViewController, I have this number displayed in a cell. In the controller there is ANOTHER timer which reloads visible cells. Everything worked, apart for the fact that once scrolling through the tableView, the number stopped changing until the table view wasn't released. I have read that the fix for this was:
RunLoop.current.add(tableTimer, forMode: RunLoopMode.commonModes)
Where tableTimer is my cell reload timer. Nevertheless, this works, but once scrolling through the view it is extremely laggy and is 0% fluid, as it normally is. Any fix? Thanks.
EDIT:
Creating the timer:
func scheduledTimerWithTimeInterval(){
reloadTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.reloadTV), userInfo: nil, repeats: true)
RunLoop.current.add(earningTimer, forMode: RunLoopMode.commonModes)
}
Updating table view:
#objc func reloadTV() {
self.tableView.beginUpdates()
self.tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
self.tableView.endUpdates()
}
Reloading a table view every .1 seconds for a label in a cell is quite stupid. For many several reasons. The optimum way to solve this issue is to loop through all visible index paths, create a cellForRowAtIndexPath with the index paths and than modify the cell title directly from there, in the following way:
#objc func reloadTV() {
for myIP in (tableView.indexPathsForVisibleRows) {
let cell = tableView.cellForRow(at: myIP)
cell.textLabel?.text = changingVariable
}
}

Swift - Calling a method in a UIViewController from a separate UIView class

Okey, I have been struggling this for a long time now.. Cant get it to work, and I know there is an easy solution that I cannot find.
I have a UIViewController displaying the main app. When it launches there is two containerViews overlaid that displays a count down for the app to start. Countdown is done, and the "overlay" viewcontrollers are removed from superview.
BUT, I need to call the function to remove VC from superview and start the app in the main UIViewController. But I am not able to trigger this function from the separate "overlay" UIView..
If I make an instance of the UIViewController to trigger the startGame() function it works, but then all the labels return nil, and the app crashes.. I believe this is because the UIViewController is triggered twice?
A lot of explaining here.. I will try to show some outtakes in code:
Main UIViewController:
class DMStartVC: UIViewController {
.....
func startGame() {
self.view.viewWithTag(100)?.removeFromSuperview()
self.view.viewWithTag(101)?.removeFromSuperview()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(DMStartVC.update), userInfo: nil, repeats: true)
UITimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(DMStartVC.UIUpdate), userInfo: nil, repeats: true)
}
....
}
The UIView handling the "overlay" container views:
class countdown: UIView {
let dmStartVC = DMStartVC()
...
//when animation is done:
dmStartVC.startGame()
}
This causes crash because all labels in the UIViewController is nil.
How can I reach that .startGame() function?
Only way I have been able to solve this is adding a timer inside the DMStartVC to trigger .startGame()... But that is not a good solution as the timer has to trigger at the exact time the countdown ends.. and it never does.

NSTimer with drawing barChart in Background

I'm using an NSTimer in an iOS App in background, which is saving some data every 30 seconds in an array. The app shows the last 10 values (values of 5 minutes) in a linechart.
My problem is to use the function of saving data into the array every 30 seconds also in background, when the app isn't on screen. I've written a lot of themes about this, but I don't understand it.
My timer is the following:
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: (#selector(ViewController.counting)), userInfo: nil, repeats: true)
func counting() {
timerCounter += 1 //Int
if timerCounter%30==0 {
arrayOfValues.append(...) //Appending the array
reloadLineChart() // reload chart
}
}
Could anyone show me how to solve this? I know, there must be something with the background-methods in the ViewController, but don't now what to type in.
I think there must be a function, which is counting in background and a function that is reloading the chart when I'm back in the app.
I might understand that you don't want to declare the counter in app delegate, for whatever reason you might have,although I would recommend it. However you can call the functions I mentioned, from the same class in which you have defined the counter. You would need to call it like this:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "applicationWillResignActive:",
name: UIApplicationWillResignActiveNotification,
object: nil)
}
func applicationWillResignActive(notification: NSNotification) {
// stop your counter
}
Then do the same with the other function. Hope is clear.
In your app delegate you can use this method :
func applicationWillResignActive(application: UIApplication) {}
The code you add in this function will run right before your app goes in the background . Therefore you can write there the code to stop the counter.
Afterwards you need to use the following function to activate the counter again:
func applicationWillEnterForeground(application: UIApplication) {}
The code you write in this function will run when you come back to the app again. Hope it is clear enough.