Swift: Issue with my NSTimer? - swift

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.

Related

Capture Application inactivity for certain time

I am trying to find out if user does not click or scroll or any other type of events in the application for 3 min; or in other words user is not interactive in the application, then the application will sign him out. similar bank application.
I could able to add GestureRecognizer as follows, but I want to catch up all the activities on the UI, is there a way to handle anything like that?
Set up a tap recognizer on collectionView:
In the viewDidLoad add the following:
let collectionViewTap = UITapGestureRecognizer(target: self, action: #selector(collectionViewTap))
collectionView.addGestureRecognizer(collectionViewTap)
Declare this function which will be called when the collectionView is tapped:
func collectionViewTap() {
print("collectionViewTap")
}
I could able to implement the time difference as follows to see timeDifference as well.
func isUserRequiredToLogin(){
let lastActivityDateAndTime = UserDefaults.standard.object(forKey: "lastActivityDateAndTime") as! Date
let currentDate = Date()
let minutes = currentDate.minutes(from: lastActivityDateAndTime)
print(minutes)
}
var timer: Timer?
func resetTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(timeIsOur), userInfo: nil, repeats: false)
}
#objc func timeIsOur() {
//logout
//log something
//alerts
}
you can add a Timer to your code and reset it in any event you want. If timer won't be reset - than timeIsOur will be executed and you do whatever you want - (logout or log something)
try to use this https://www.zerotoappstore.com/how-to-detect-user-inactivity-swift.html It works in my production app.

Update a progress bar while JSONDecoder is decoding a huge object, in Swift

I'm decoding several gigabytes of JSON encoded data like this
let decoder = JSONDecoder()
let table = try decoder.decode([LogRow].self, from: content!)
where content is plain text. Now, this operation can take several minutes, depending on the size of content, and I'd like to show some kind of progress. This is a command line program, so even a periodic update on the length of table would be enough. The problem is that I don't see anything like a callback or something like that. I tried with a rather awkward Timer like this
var table: [LogRow]? = []
let timer = Timer(fire: Date(), interval: 1.0, repeats: true) { t in
print("\(table?.count ?? 0) rows parsed.")
}
timer.fire()
table = try decoder.decode([LogRow].self, from: content!)
timer.invalidate()
but that only runs once -- is it because the decoder blocks the main thread and I'm running the timer in the same thread? I'm a bit of a noob with the GCD so I'm not sure how to go about using a DispatchQueue for this.
Any ideas?
Then can declare your timer like:
let timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateUI), userInfo: nil, repeats: true)
DispatchQueue.global(qos: .background).async {
self.perform(#selector(self.decode), on: Thread.current, with: nil, waitUntilDone: true)
timer.invalidate()
}
which means you want to trigger the updateUI action every second. Then you start to decode in a background thread and wait until done before invalidate your timer.
var totalDuration: Int = 0
#objc func updateUI () {
self.currentDuration += 1
print("------------> \(self.currentDuration)")
}
#objc func decode () {
table = try decoder?.decode([LogRow].self, from: content!)
}
I added a currentDuration variable which could be used in your progressBar. But you must know the total duration if you need to show a percentage to your user.

Cancel Swift Timer From Another Function

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()
}

Swift NSTimer Pause Resume

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.

How to delay the return to the calling function swift

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
}