In my app, I put a label on the view controller and I want that label to display a number which is incremented by 1 every millisecond. (I know I can just update it every second by 1000, but I want it to look smooth)
#IBOutlet var label: UILabel!
var number = 0
var timer: NSTimer!
override func viewDidLoad() {
super.viewDidLoad()
timer = NSTimer.scheduledTimerWithTimeInterval(0.001, target: self, selector: Selector("addNumber"), userInfo: nil, repeats: true)
}
func addNumber () {
number++
label.text = number.description
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
timer?.invalidate()
}
The code all looks very sensible to me, but when I run the app, I see that the number is increasing by about 50 every second, not 1000 every second. That error is far from acceptable!
Surprisingly, when I lock the screen for a few seconds and unlock it again, the number suddenly increases by a few thousands!
I think this is because I am updating the UI when the timer fires so it requires more time. But how can I fix this?
Your question is asking how to make NSTimer more accurate and I have some bad news: you can't. A reasonable expectation of NSTimer is that it can be called ~30 times per second and, if you're lucky, up to ~60 times per second. And that's okay.
Thinking of this from a practical point of view: the display can only be updated up to 60 times per second, so there is no need to give user feedback more often than that.
If you're trying to max out the hardware and rely on it firing as fast as possible, then you're going to have issues on older and slower hardware where it might only fire ~20 times per second.
Relying on the time interval of NSTimer will get you in trouble quickly.
Since you're looking for milliseconds, you can set a start time and every time your function is called look at the number of milliseconds since your start time and that's the number of transpired milliseconds.
Your current method does everything Apple's documentation says it should: number is just the count of times that method has been called (not the number of transpired milliseconds).
Building on #Fennelouski's answer, what I suggest is you swap to a CADisplayLink which is pretty much made for this kind of task. It will call your method once every screen update (or once every 2nd or 3rd, or whatever you make the frameInterval property). it also has a timestamp variable that you can use to calculate how much time has passed and update your label accordingly
Related
I have UITabBarController and in one of the UIViewController there I scroll UICollectionView each 5 second using Timer. Here is short code how I do it:
override func viewDidLoad() {
super.viewDidLoad()
configureTimer()
}
private func configureTimer() {
slideTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(scrollCollectionView), userInfo: nil, repeats: true)
}
#objc func scrollCollectionView() {
collectionView.scrollToItem(at: someIndexPath, at: .centeredHorizontally, animated: true)
}
It perfectly works. But I think it has a big issue. Of course, I can open another screen from this UIViewController (for example, I can tap to another tab or push another UIViewController). It means, my UIViewController's view, containing UICollectionView, disappears. In another words, viewDidDissapear will be called. But my timer still exists and I am having strong reference to it, possibly, there is retain cycle. It keeps working and each 5 second scrollCollectionView method is called even my view dissapeared. I don't know how, but iOS somehow handles it. In other words, it can modify view even it is not visible. How is that possible and is it good practice? Of course, I can invalidate my timer in viewDidDissapear and start it in viewDidAppear. But I don't want to loose my timer value and don't want to start it from zero again. Or may be it is ok to invalidate my timer in deinit?
My question covers pretty common situation. For instance, if I make network request and open another UIViewController. After that request finished, I should modify UI, but now am on another screen. Is it ok to allow iOS to modify UI even it is not visible?
A couple of thoughts:
If the timer is updating the UI at some interval, you definitely should start it in viewDidAppear and stop it in viewDidDisappear. There’s no point in wasting resources updating a view that is not visible.
By doing this, you can solve your strong reference cycle, too.
In terms of “losing” your timer value and starting at zero, we generally would just save the time you’re counting from or to and calculate the necessary value when restarting the timer later.
We do this, anyway, because you really shouldn’t be using timers to increment values because you’re technically not assured that they’ll be called with the frequency you expect.
All of that said, I don’t know what timer “value” you’re worried about losing in this example.
But definitely don’t waste time updating a UI that is no longer visible. It’s not scalable and blurs the distinction between the model (what you’re counting to or from) from the UI (the update that happens every five seconds).
I'm trying to create a UITableView that contains a bunch of cells that count down from a value. Initially I was just running an NSTimer in the ViewController on repeat to run a function that would update the labels and then run a reloadData on the table, and that does technically work, but with some issues.
To fill things in a bit more, I'm well aware NSTimer isn't accurate as a timer, so I use a date calculation for the time on the labels. I also considered using CADisplayLink so the refresh would sync with the screen, but this ran into problems with scrolling the table, making it very jumpy. Another issue is when issuing the Edit command on the table, the UI is unable to delete rows because of the refresh.
So, I've since considered moving my Timer into each UITableViewCell, and I can tell that it is running via the console, but the table doesn't update. This makes sense, given that it can't run the reloadData on the table, but I'm a bit stuck as to where to go next.
I found a similar thread here and even tried the AsyncTimer suggested here but either I'm doing something wrong, or these aren't quite the same issues? Any help is appreciated, thank you!
var timer = Timer()
self.timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.checkTimers), userInfo: nil, repeats: true)
#objc func checkTimers() {
for counter in counters {
if counter.isActive {
counter.updateSeconds()
}
}
tableView.reloadData()
}
When selecting a row the following code sets up a set of AVAudioPlayers to playback at a certain date (in this case, 50 players playing in the interval of 1 second).
Since I want the whole process to restart when touching again I need to break the setup in the for loop since it takes a few seconds to setup the players.
Apart from that, each player is being removed after finishing playback using the audioDidFinishPlaying delegate method of AVAudioPlayerDelegate. I did not include this in the code since it is not relevant to the question.
I've tried using a flag inside the for loop to check whether setup is allowed but that doesn't work.
var players: [AVAudioPlayer] = []
var loadError: NSError?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Removes the players that have been setup already.
players.removeAll()
// The for loop from the previous row selection should be stopped here.
for i in 0..<50 {
do {
let player = try AVAudioPlayer(contentsOfURL: soundUrls[i])
players += [player]
// The process of setting these up takes a few seconds, I need to break it.
print("Firing timer")
player.playAtTime(player.deviceCurrentTime + NSTimeInterval(i))
} catch let error as NSError {
loadError = error
}
}
}
What happens is, that the setup triggered by the previous row selection will continue until it is finished and only then the new for loop starts.
I need to break it earlier.
I can't figure out how to tackle this. Maybe by removing the processes from the main thread(How?)? Any help much appreciated!
I'm a little bit confused about your statement of the problem, but I'll try to give a suggestion anyway.
You say that you set up the players when selecting a row, but the code to set them up is in cellForRowAtIndexPath. So it's set up and playing starts when a cell is returned and displayed in your table view.
What exactly are you trying to achieve? You have a table view with a number of rows, and whenever you tap on a cell the fifty sounds have to start playing one after the other (1 second apart). If you tap the same cell again they should stop and restart, is that it?
Then what I would do is set up the 50 players in viewDidLoad of your TableViewController. Use prepareToPlay().
Start them when needed.
Then if you need to restart them, just cycle through them, check if they are still playing using isPlaying. Pause them if needed, set the current time to 0 and call playAtTime again.
Don't remove the players in audioDidFinishPlaying. Because then you'd have to recreate them.
Just reset them so they're available for immediate playback again.
By the way, if you're going to do more with audio and want more control and better performance I highly recommend the excellent frameworks The Amazing Audio Engine 2, or AudioKit
I'm creating an iOS test game as practice and want a game over method to be ran at the end of a 15 second timer.
I also want time to be added to this timer and taken away if certain actions take place.
Not having much luck figuring out how to add time or take away time from a certain timer depending on actions by the user.
So far I have the timer in viewDidLoad and a method with some conditional formatting that will do things depending on what the user does.
Once it has been created, the period of an NSTimer cannot be changed, the timer can only be cancelled or allowed to fire after the requested period.
In order to implement a timer for your game I would suggest that you set up a repeating timer that fires every second. The actual 'time remaining' value is stored in an integer and you decrement this value each time the timer fires. When this value reaches 0, end the game.
This way you can easily add additional time by simply changing the value of the time remaining variable.
This approach also makes it simple to display the remaining time on the screen.
Once NSTimer has been created, you cannot change time interval. But here's method I've done for me:
func addTimeToTimer(timer: NSTimer, time: NSTimeInterval, target: AnyObject, selector: Selector, repeats: Bool) -> NSTimer {
let currentInterval = Float(timer.timeInterval)
let targetInterval = NSTimeInterval(currentInterval + Float(time))
let newTimer = NSTimer.scheduledTimerWithTimeInterval(targetInterval, target: target, selector: selector, userInfo: timer.userInfo, repeats: repeats)
return newTimer
}
You will need to supply a lot of parameters because NSTimer does not store it all. Hope it helps.
I'm using NSTimer to fire a method that scrolls UITableView
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:#selector(scroller)
userInfo:nil
repeats:YES];
-(void)scroller
{
[self.row1TableView setContentOffset:CGPointMake(self.row1TableView.contentOffset.x, self.row1TableView.contentOffset.y - 50) animated:YES];
}
Problem is that the scrolling seems slow. It's closer to 1 second than .1 seconds in the interval.
What's the problem?
As far as I know, you can not change the default animation duration of setContentOffset:animated:. But what you can do is, setup a Core Animation display link (CADisplayLink - you can search for code samples on how to set up, but it is quite straight-forward. The class documentation should be a good place to start) and it will fire every frame, calling back a method you provide.
Inside that callback method, you can calculate how much you want to scroll your table view (how many points per frame), and call setContentOffset:animated: with the second parameter set to NO (immediate scrolling). You should implement some sort of easing to achieve better results.
Note: The reason for using CADisplayLink instead of NSTimer is, it is more reliable. It is what you would use in games before SpriteKit was available.
Addendum: This blog post has some sample code on how to setup the display link and the respective callback method.
Addendum 2: You can setup an instance variable to act as a "counter", and increment it by the ammount of time ellapsed since last frame, within each call of your callback (use properties duration and/or frameInterval). Once the counter reaches a critical value (that is, the animation has run for enough time) you can stop the display link update by calling the method:
-[CADisplayLink invalidate].
NSTimer that calls a selector on the current threads run loop. It may not be 100% precise time-wise as it attempts to dequeue the message from
the run loop and perform the selector.