I am programming with Swift, and I have a problem.
I am writing code using a timer using NSTimer, and I have succeeded in stopping the timer. I created a button to restart the timer, but I do not know how to enter the code.
import UIKit
class ViewController: UIViewController {
/*Timer1*/
var landmarkTime = 0
var landmarkNSTimer = Timer()
/*Timer3*/
func landmarkTimer() {
landmarkTime += 1
}
/*PauseButton*/
#IBAction func PauseButton(_ sender: Any) {
landmarkNSTimer.invalidate()
}
/*ResumeButton*/
#IBAction func ResumeButton(_ sender: Any) {
/*I want to restart the timer in this part, but I do not know what code to put in.*/
}
override func viewDidLoad() {
super.viewDidLoad()
/*Timer2*/
landmarkNSTimer = Timer(timeInterval: 1, target: self, selector: #selector(ViewController.landmarkTimer), userInfo: nil, repeats: true)
RunLoop.main.add(landmarkNSTimer, forMode: RunLoopMode.commonModes)
}
}
/* ResumeButton */ This is a problem. I typed /* Timer2 */ in this part, but I have an unknown problem and am looking for another code. Tell the new code to be entered in /* ResumeButton */ so that the timer can be restarted.
You didn't pause the timer, you stopped it. If you read the documentation for invalidate, it says [emphasis added]:
Stops the receiver from ever firing again and requests its removal from its run loop.
Timers don't pause; it's simply not part of their API. If you want a pause/resume behavior, there are a variety of ways to get it, including:
Create a new timer when resuming.
Set the fireDate to Date.distantFuture when pausing, then set it to some time in the near future when resuming.
Use a boolean variable to indicate the paused state, and have your timer callback check that variable when the timer fires. If paused == true, do nothing.
Related
If you run the code below, even after I invalidate the timer, the remaining code of the timer executes without any disruption. Why?
Is it because the closure has a strong reference to itself and is retained until it completely finishes itself? Or something else?
Does this mean invalidating a timer during its moment of execution does nothing?
class ViewController: UIViewController {
var timer : Timer?
let serialQueue = DispatchQueue(label: "com.createTimer.serial")
override func viewDidLoad() {
super.viewDidLoad()
serialQueue.sync { [weak self] in
self?.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false, block: { [weak self] _ in
self?.someDummyFunc()
print("yyy")
print("\(self?.timer?.isValid)")
})
}
}
func someDummyFunc(){
print("\(timer?.isValid)")
print("xxx")
timer?.invalidate()
}
}
The prints that I get from running this code is:
Optional(true)
xxx
yyy
Optional(false) // timer.isValid is false !!!
Yet what I initially thought I would get is:
Optional(true)
xxx
The scheduledTimer(withTimeInterval:repeats:block:) method:
After interval seconds have elapsed, the timer fires, executing block.
The invalidate() method:
Stops the timer from ever firing again
You are correct in your discovery that invalidating a timer will not interrupt a currently executing block, but will only prevent future executions of that block.
Timer or not, any enqueued block will have to finish. Once it's enqueued there's no stopping it.
Suppose you had the following code inside a viewController's:
override func viewDidLoad() {
super.viewDidLoad()
let x = 10
DispatchQueue.main.async { [weak self] in
print(x)
self?.someTaskWhichTakes3seconds()
print(self?.view.backgroundColor)
print(x + 2)
}
}
and after viewDidLoad was called you immediately popped the viewController off the navigation stack (or had it deallocated somehow), in that case, still print(x+2) would happen. Why? Because the block is enqueued and it has to finish.
I am using a Swift Timer library found here. It allows you to run a timer and then stop it using the following syntax:
Timer.every(5.seconds) { (timer: Timer) in
// do something
if finished {
timer.invalidate()
}
}
I am trying to have the timer start and then have the option of the timer being cancelled from another function but I can't figure out how to reference the timer that is counting down from another function. I have tried doing something like this but it throws the following error:
Static member every cannot be used on instance of type Timer
var countTimer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
initNotificationSetupCheck()
countTimer.every(5.seconds) { //error here
}
func stopTimer() {
countTimer.invalidate()
}
What if you were to use a mixture of both, but instead of creating an instance of Timer, assign the timer from inside the closure to your member variable like:
var countTimer: Timer
Timer.each(5.seconds) { timer in
countTimer = timer
//rest of code
}
Then you can invalidate the timer outside of the closure if needed.
You can go like this
self.timer = Timer.scheduledTimer(timeInterval: 5, target: self,
selector: #selector(self.eventForTimer), userInfo: nil, repeats: true)
for cancelling timer
func cancelTimer() {
self.timer.invalidate()
}
Theres any way to keep app running while user doesn't kill the app? I found that this can be accomplish with RunLoop.current.run() but when hit the app in this like the app completely freeze, I've a Timer that its declared before the RunLoop and seems to be correctly initialized but the timer doesn't proc, heres a simple example
import Foundation
print("start")
let _ = Timer.init(timeInterval: 1, repeats: true) { _ in
print("proc")
}
RunLoop.current.run()
print("end")
Seems that I'm not catching how this works.
Regards
The problem is merely that you don't know how to use a Timer. It isn't enough to init it. You have to schedule it. Instead of calling init..., call Timer.scheduledTimer... and the whole thing will spring to life for you. Then you'll see that in fact your process is running.
I put this (I can't use a block-based Timer because I'm not using Sierra) and it worked fine:
import Foundation
class TimerHolder:NSObject {
var timer : Timer?
func timerFired(_:Timer) { print("fired") }
override init() {
super.init()
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self,
selector: #selector(timerFired),
userInfo: nil, repeats: true)
}
}
print("start")
_ = TimerHolder()
RunLoop.current.run()
print("end")
So in general terms, this is what i want my program to do: music will play, randomly generate a number between 3 and 12, then set a timer according to that number, when the timer is finished stop the music for 1 second, then after that one second, play the music again(and repeat the process, generate random number...)
This is the code I have written to do so, it works great the first run, but the second run it starts calling the functions at a faster rate(not following the timer) and sometimes in a different order(you can test this by looking at the print out)
var randomTimeInteger = Int()
var stopMusicTimer = NSTimer()
var startMusicTimer = NSTimer()
override func viewDidLoad(){
playMusic()
}
//Call to get random interger
func generateRandomInteger(){
print("Inside generate random integer")
let lower = 3
let upper = 12
randomTimeInteger = lower + Int(arc4random_uniform(UInt32(upper - lower + 1)))
print("Random Integer is : \(randomTimeInteger)")
initiateTimer()
}
//Start the timer
func initiateTimer() {
print("Inside initate Timer")
//Set Timer
stopMusicTimer = NSTimer.scheduledTimerWithTimeInterval(Double(randomTimeInteger), target: self, selector: "stopMusic", userInfo: nil, repeats: true)
}
//Stop the music
func stopMusic(){
print("Inside stop music")
GameViewController.backgroundAudio?.stop()
startMusicTimer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: "playMusic", userInfo: nil, repeats: true)
}
func playMusic(){
print("Inside play music")
GameViewController.backgroundAudio?.play()
generateRandomInteger()
}
Any idea what the problem is? Or if there is a better way to do so? PLEASE HELP!
You have one timer that tries to stop the music every 3 to 12 seconds. And sometimes you create a timer that tries to start the music every 3 seconds. And sometimes you create more of these timers. So eventually you have lots and lots of timers that try starting and stopping the music at random time.
To stop a repeating timer, call invalidate.
And don't initialise the timers as you do, NSTimer() returns nothing useful. Just declare the variables as NSTimer?
There are really numerous viable approaches.
Starting from the top, I would sketch the design as follows:
func playMusic(completion: (ErrorType?)->()) {
play(randomDuration()) { error in
if error != nil {
completion(error)
return
}
delay(1.0, f: play)
}
}
func randomDuration() -> Double {...}
func play(duration: Double, completion: (ErrorType?)->()) {
let player = Player(...)
player.resume()
delay(duration) {
player.suspend()
completion(nil)
}
}
Function delay(_:f:) is implemented in terms of dispatch_after.
You will probably notice, that playMusic runs indefinitely. This is by your requirements, but in practice you need a way to stop it.
In my application the user can press a button. That in turn leads to a function call which is showed below:
In ViewController.Swift
#IBAction func pickMeUpButton(sender: AnyObject) {
sendPushNotificationController().sendPushNotification("sendRequest",userInfo: defaults.stringForKey("x73")!, userInf23: defaults.stringForKey("x23")! )
locationController.getLocationForShortTime() // --> here i want the timer to finish the 5 seconds before proceeding
activityIndicator.center = self.view.center
activityIndicator.startAnimating()
self.view.addSubview(activityIndicator)
//activityIndicator.stopAnimating()
}
And this is the class function where the call is being made to
In getUserLocation.swift
func initManager(){
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
}
func getLocationForShortTime(){
initManager()
timer = NSTimer.scheduledTimerWithTimeInterval(5, target: self, selector: "stopGettingLocation", userInfo: nil, repeats: false)
}
func stopGettingLocation(){
locationManager.stopUpdatingLocation()
}
So this will make the application get the users location for 5 seconds and then the timer will stop the updates. What i want to do is when the five seconds has elapsed and the location update stops THEN i would like the calling function to proceed to next line.
I though of some solutions using boolean, but it is not a nice solution. Im thinking there might be a better way to do this?
Others have told you what to do, but not why.
You need to adjust your thinking.
With an event-driven device like an iPhone/iPad, you can't stop processing on the main thread for 5 seconds. The UI would lock up, and after a couple of seconds the system would kill your app as being hung.
Instead, what you do is to invoke a block of code (a closure) after a delay.
You could rewrite your function like this:
#IBAction func pickMeUpButton(sender: AnyObject)
{
sendPushNotificationController().sendPushNotification("sendRequest",
userInfo: defaults.stringForKey("x73")!,
userInf23: defaults.stringForKey("x23")! )
initManager()
//Start the activity indicator during the delay
activityIndicator.center = self.view.center
self.view.addSubview(activityIndicator)
activityIndicator.startAnimating()
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, Int64(5.0 * Double(NSEC_PER_SEC))),
dispatch_get_main_queue())
{
//The code in the braces gets run after the delay value
locationManager.stopUpdatingLocation()
activityIndicator.stopAnimating()
}
//dispatch_after returns immediately, so code here will run before
//the delay period passes.
}
That button action code will:
Call initManager to start the location manager running.
Immediately create an activity indicator, add it to the view controller's content view, and start it spinning.
Then, the call to dispatch_after will wait for 5 seconds before running the code in the braces, which will stop the location manger and stop the activity indicator.
For delaying a function-call you can use dispatch_after. It's syntax is a little bit ugly so you can also use this delay function:
/// delays the execution of the passed function
func delay(delay: Double, closure: ()->()) {
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))),
dispatch_get_main_queue(),
closure)
}
// calling directly (locationManager gets captured; which is in this case negligible to consider)
delay(5.0, closure: locationManager.stopUpdatingLocation)
// or indirect through your function
delay(5.0, closure: stopGettingLocation)
Pass the closure to getLocationForShortTime. The one that should be run once the thing is finished. I can't really test the code, but it's probably something like:
class Handler { // <- This is the wrapper class for completion closures
private let run: Void -> Void
init(_ run: Void -> Void) { self.run = run }
}
lazy var locationManager: CLLocationManager! = self.lazyLocationManager()
func lazyLocationManager() -> CLLocationManager {
let _locationManager = CLLocationManager()
_locationManager.delegate = self
_locationManager.desiredAccuracy = kCLLocationAccuracyBest
_locationManager.requestAlwaysAuthorization()
return _locationManager
}
func getLocationQuick(onComplete: Void -> Void) { // <- This takes the closure
locationManager.startUpdatingLocation()
let timer = NSTimer.scheduledTimerWithTimeInterval(
5,
target: self,
selector: "gotLocationQuick:",
userInfo: Handler(onComplete), // <- Here you wrap the completion closure
repeats: false // and pass it to the timer
)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
}
func gotLocationQuick(timer: NSTimer) {
locationManager.stopUpdatingLocation()
let completionHandler = timer.userInfo as! Handler
completionHandler.run()
}
#IBAction func pickMeUpButton(sender: AnyObject) {
sendPushNotificationController().sendPushNotification(
"sendRequest",
userInfo: defaults.stringForKey("x73")!,
userInf23: defaults.stringForKey("x23")!
)
activityIndicator.center = self.view.center
self.view.addSubview(activityIndicator)
activityIndicator.startAnimating()
locationController.getLocationQuick() { // <- You post the request for
activityIndicator.stopAnimating() // geolocation and indicate the code
} // to be run once it's finished
}