scheduledTimer not stopping another ViewController in swift 5 - swift

I have set scheduledTimer in a view Controller. But when got to anther view controller, previous view controller timer is continue.Here is my code
var timer: Timer? = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: false)
Here is function
#objc func runTimedCode() {
//Api Calling
}
when I go to anther viewController, previous Api calling is continue.
Please help me to scheduledTimer go another page/viewcontroller

It's likely that you may not be calling invalidate on the same thread as where you installed the timer. For example, if you installed in on the main thread, wrap invalidate in:
DispatchQueue.main.async(execute: {
self.timer.invalidate()
})
Reference: https://developer.apple.com/documentation/foundation/timer/1415405-invalidate

Timer holds strong reference to current view controller even if your app goes into background or inactive state it still active . Also whenever you push new controller timer is not stoped at all .
Even if you pop your controller it still don't stop timer at this moment . However timer would start with double speed if you initialise timer again . let's understand this problem .
Lets have 3 controllers
HomeScreenVC --> TimerControllerVC --> NewVC
When you initialise timer in TimerController . It create strong reference to your TimerController . Now push NewVC controller on some action method , timer is not stopped at this moment it is still running .
Same case happens with if you pop TimerControllerVC controller to HomeScreenVC . Now you again pushing TimerControllerVC and initialising fresh timer in some method . This time timer would work with 2x speed irrespective of new timer created timer . It creates cycle of recreation if you repeat this step .
Resolving timer issue , remember whenever you declare timer , you must invalidate it in viewwilldissappear method of UIViewcontroller .
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer.invalidate()
}
Invalidating timer in some other self declare methods will not helps because there are always some cases when your self declare is not called which will lead to some unexpected issue with timer .

Related

Timer on table view Cell

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.

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.

How can I create a segue after a certain amount of seconds between view controllers without any buttons or swipe gestures?

At the entrance of my main storyboard, I have a loader animation from the Lottie library, after a few seconds I want the app to animate to another view controller which is my home page, How do I do this? I would prefer to not use a button or swipe gesture to have to go from the loader to the home page, but would I have do use the Timer class in some way, or would I need to create a segue using storyboard?
I already attempted using other questions similar to this on Stack Overflow, but I continuously get errors about performSegue() not having an identifier
2 ways: Segue or present(_ viewController:UIViewController, animated:Bool, completion: () -> Void)
So, in your 1st ViewController viewDidLoad method you can implement a Timer. Such as, Timer.scheduledTimer(withTimeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void) and set a time, set repeat to false and then the block, you can do whatever you want, such as present a new view controller. Example:
override func viewDidLoad() {
Timer.scheduledTime(withTimeInterval: 10.0, repeats: false) { (timer) in
self.present(ViewController2(), animated: true, completion: nil)
}
}

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.