Is there a bug with simulator when we trying to make a timer with timeInterval < 0.1 second? - ios

I'm making a new stopwatch Application with Watchkit, first my application is very simple like this:
first of all i tried to make a playButtonPressed to start a timer:
#IBAction func playButtonPressed() {
println("playButton pressed")
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("updateTimer"), userInfo: nil, repeats: true)
startTime = NSDate()
}
with my updateTimer function like this:
func updateTimer() {
duration = NSDate().timeIntervalSinceDate(startTime)
println("updateTimer: \(dateStringFromTimeInterval(duration))")
timeLabel.setText(dateStringFromTimeInterval(duration))
}
the dateStringFromTimeInterval function can help me make a dateString with duration is TimeInterval variable.
every thing is ok with my output on Debug area, i can see the dateString at printOut. But my label is lagging for setting the timeLabel as you can see here:
I don't know why, can any one can help me fix that or may be it is a bug of apple watchkit right now? i don't know it'll be lag on real device or not.
Thanks

You have several good questions in here. Unfortunately I have nothing but bad news for you. I have been working extensively with WKInterfaceTimers over the past couple of weeks and they have severe limitations and bugs associated with them. I have a couple responses broken down here in detail.
Issue 1 - Using a WKInterfaceDate as a Timer
This is going to be really frowned upon by Apple and I wouldn't doubt this would be possible grounds for rejection. As #mattt mentions, you don't want to use an NSTimer to flip the date value. Each time you try to switch the date label, Apple lumps all those changes together and pushes them from the Extension on the iPhone to the Watch over WiFi or Bluetooth. It does this to try to optimize battery life.
Because of this, you will never be able to accurately display the time on the Watch in the way that you are currently doing. The proper way to do this is to use a WKInterfaceTimer.
Issue 2 - Using a WKInterfaceTimer
While WKInterfaceTimers are built to do exactly what you want, they have some limitations. The major one for your use case is that it only does up to second precision, not millisecond. Secondly, the timers are extremely inaccurate. I've seen them anywhere from 50ms to 950ms off. I've followed this radar to bring the issue to Apple's attention.
In summary, your current implementation is not going to be super accurate and will be frowned upon by Apple, and the WKInterfaceTimer is extremely inaccurate and can only perform second precision.
Sorry for the downer answer. :-(

Related

Timer is not starting after explicitly telling it to [duplicate]

This question already has answers here:
Why would a `scheduledTimer` fire properly when setup outside a block, but not within a block?
(3 answers)
Swift Timer.scheduledTimer() doesn't work
(2 answers)
Closed 3 years ago.
This is slowly beginning to drive me insane because it doesn't make sense...
I'm using Swift and can't seem to get this timer to start no matter what I do.
I have an NSObject that's handling all my time-related things in my app, and in that NSObject I have a function that initializes a timer. It looks like this:
class Time: NSObject {
static let sharedInstance = Time()
private override init(){}
var roundTimer = Timer()
//This is called from my splash screen while the app is loading
func initializeTimers(){
//Initialize the round countdown timer
roundTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(updateRoundTimerCountdown)), userInfo: nil, repeats: true)
print("The round timer should have started")
}
#objc func updateRoundTimerCountdown(){print("Round Timer ran"); Time.roundTimerIsAt -= 1; print("Round Timer: \(Time.roundTimerIsAt)")}
}
From my splash screen where the app loads all the user data, I call Time.sharedInstance.initializeTimers() in order to start it. I get to the point of it printing "The round timer should have started" to the debugger (which happens after the line that should start the timer) but the selector isn't hit at all. The "Round Timer ran" isn't printed to the debugger nor does the timer appear to have started, so what am I doing wrong?
I appreciate any help, even if the answer is glaringly obvious lol :P I've spent too much time on this!
I actually finally got it working! I'm sorry...there would have been too much code if I posted my entire splash screen view controller and Time NSData, but it appears the issue was elsewhere. But maybe someone else may run into this so this may help.
On my splash screen, I perform 15 'app-loading steps' that include several steps that gather information from servers and such... those steps I progress through by using completion handlers to ensure that the data is actually collected.
It's after collecting some of this data that I call the Time.sharedInstance.initializeTimers() function. My call to the servers runs on a different thread other than the main, so apparently by calling this function after the completion handler runs, it's still on the other thread and these timers can't start on anything other than the main thread!?
So all I did was this:
DispatchQueue.main.async {Time.sharedInstance.initializeTimers()}
And it works now... (facepalm). Doesn't make a whole lot of sense to me but... it works :P

iOS - AVAudioPlayerNode.play() execution is very slow

I'm using AVAudioEngine for audio in an iOS game application. A problem I've encountered is that AVAudioPlayerNode.play() takes a long time to execute, which can be a problem in real-time applications such as games.
play() just activates the player node - you don't have to call it every time you play a sound. As such, it doesn't have to be called that often, but it does have to be called occasionally, such as to activate the player initially, or after it's been deactivated (which happens in some situations). Even if only called occasionally, the long execution times can be a problem, especially if you need to call play() on multiple players at once.
The execution time for play() seems to be proportional to the value of AVAudioSession.ioBufferDuration, which you can request to be changed using AVAudioSession.setPreferredIOBufferDuration(). Here's some code I'm using to test this:
import AVFoundation
import UIKit
class ViewController: UIViewController {
private let engine = AVAudioEngine()
private let player = AVAudioPlayerNode()
private let ioBufferSize = 1024.0 // Or 256.0
override func viewDidLoad() {
super.viewDidLoad()
let audioSession = AVAudioSession.sharedInstance()
try! audioSession.setPreferredIOBufferDuration(ioBufferSize / 44100.0)
try! audioSession.setActive(true)
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: nil)
try! engine.start()
print("IO buffer duration: \(audioSession.ioBufferDuration)")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if player.isPlaying {
player.stop()
} else {
let startTime = CACurrentMediaTime()
player.play()
let endTime = CACurrentMediaTime()
print("\(endTime - startTime)")
}
}
}
Here are some sample timings for play() that I got using a buffer size of 1024 (which I believe is the default):
0.0218
0.0147
0.0211
0.0160
0.0184
0.0194
0.0129
0.0160
Here are some sample timings using a buffer size of 256:
0.0014
0.0029
0.0033
0.0023
0.0030
0.0039
0.0031
0.0032
As you can see above, for a buffer size of 1024, execution times tend to be in the 15-20 ms range (around a full frame at 60 FPS). With a buffer size of 256, it's around 3 ms - not as bad, but still costly when you only have ~17 ms per frame to work with.
This is on an iPad Mini 2 running iOS 12.4.2. This is obviously an old device, but the results I see on the simulator seem similarly proportional, so it seems to have more to do with the buffer size and the behavior of the function itself than with the hardware being used. I don't know what's going on under the hood, but it seems possible that play() blocks until the beginning of the next audio cycle, or something like that.
Requesting a lower buffer size seems like a partial solution, but there are some potential drawbacks. According to the documentation here, lower buffer sizes can mean more disk access when streaming from a file, and irrespective of that, the request may not be honored at all. Also, here, someone reports playback problems related to low buffer sizes. Taking all this into account, I'm disinclined to pursue this as a solution.
That leaves me with execution times for play() in the 15-20 ms range, which typically means a missed frame at 60 FPS. If I arrange things so that only one call to play() is made at a time, and only infrequently, maybe it won't be noticeable, but it's not ideal.
I've searched for information and asked about this in other places, but it seems either not many people are encountering this behavior in practice, or it isn't an issue for them.
AVAudioEngine is intended for use in real-time applications, so if I'm right that AVAudioPlayerNode.play() blocks for a significant amount of time proportional to the buffer size, that seems like a design issue. I realize this probably isn't an issue many are dealing with, but I'm posting here to ask if anyone has encountered this specific issue with AVAudioEngine, and if so, if there's any insight, suggestions, or workarounds anyone can offer.
I've investigated this fairly thoroughly. Here are my findings.
Having now tested the behavior on a variety of devices and iOS versions (including the latest version at the time of this writing, 13.2), and having had others test it as well, my current conclusion is that the long execution times for AVAudioPlayerNode.play() are inherent and that there's no obvious workaround. As noted in my original post, the execution times can be reduced by requesting a lower buffer duration, but as discussed earlier, this doesn't seem like a viable solution.
I heard from a credible source that calling play() on a background thread (e.g. using Grand Central Dispatch) should be safe, and indeed this would be one way to solve the problem. However, although it may technically be safe to call play() (or other AVAudioEngine-related functions) on different threads, I'm skeptical as to whether this is a good idea (further explanation below).
The documentation doesn't state this as far as I can tell, but AVAudioEngine will throw NSException's under various circumstances, which, without special handling, will result in application termination in Swift.
One of the things that will cause an NSException to be thrown is if you call AVAudioPlayerNode.play() while the engine is not running. Obviously if you only have your own code to worry about, you can take steps to ensure this doesn't occur.
However, iOS itself will sometimes stop the engine of its own accord, for example when an audio interruption occurs. If you call play() subsequent to that and before restarting the engine, an NSException will be thrown. It's fairly easy to avoid this mistake if all your calls to play() are on the main thread, but multithreading complicates the issue and seems like it could introduce the risk of accidentally calling play() after the engine has been stopped. Although there may be ways to work around this, multithreading seems to introduce undesirable complexity and fragility, so I've opted not to pursue it.
My current strategy is as follows. For the reasons discussed earlier, I'm not using multithreading. Instead, I'm doing everything I can to reduce the number of calls to play(), both overall and per-frame. This includes, among other things, only supporting stereo audio (for various reasons, supporting both mono and stereo can lead to more calls to play(), which is undesirable).
Lastly, I also investigated alternatives to AVAudioEngine. OpenAL is still supported on iOS, but is deprecated. A custom implementation using low-level APIs such as Audio Queue Services or Audio Units would be a possibility, but would be non-trivial. I've also looked at some open-source solutions, but the options I looked at use AVAudioEngine under the hood themselves and therefore suffer from the same problems, and/or have other shortcomings or limitations of their own. Of course there are also commercial options available, which may provide a solution for some developers.

Timer Loops - not NSTimer - Swift

There are lots of questions about using NSTimer for timer functions but I am instead using a CocoaPod "Countdown Label" https://github.com/suzuki-0000/CountdownLabel
My question/problem specifically is how to trigger an action/notification once it is complete I don't believe this to be the same as the many "NSTimer" questions/tutorials but please correct me if I'm wrong!
I've read the Timer documentation here https://developer.apple.com/documentation/foundation/timer but, again, don't see how it would apply to a 'custom timer library' like this Cocoapod.
I am currently trying this....
func testTimer() {
timerLabel.countdownDelegate = self
timerLabel.start()
if self.timerLabel.isFinished {
print("timer it's finished")
} else {self.timerLabel.start()
}
}
testTimer() is then called when an IBAction is pressed to star the timer.
My logic/thinking was that this function starts the timer, then checks if it finished. If it isn't finished it starts/continues the timer (timerLabel.start) and checks again basically in a loop until it is finished and then prints "timer is finished" but this isn't working and I'm not sure why? (but builds fine)
I know I could just scrap the pod and follow a Timer() tutorial but I'm trying to learn/understand how this sort of 'internal notification' for non Apple libraries would work generally at the same time as solving this specific problem. I hope this all makes sense.
NB. Some of the questions/answers I've read through that I don't believe are what I need are How to check if a NSTimer is running or not in Swift?, Check if Timer is running, Perform segue when timer is finished but they all use the Apple "Timer()".
NB. I am setting up the timer is ViewDidLoad with data from a Segue like this :
let timerLabelTime = Int(selectedWorkoutTime)
timerLabel.setCountDownTime(minutes: Double((timerLabelTime)!*60))
timerLabel.countdownDelegate = self
timerLabel.pause()
which works fine

Pushing A New WatchKit Controller Immediately After Popping Another Always Fails

OK. It's easy enough to do this in classic iOS, but WatchKit doesn't give any blocks/closures, and there isn't a choice between with/without animation.
I have a root controller that has a list of options. Touching one of the options (on either the watch or the phone) will close any currently open controller (popToRootController), then immediately, push a new one.
More or less, like so:
self.popToRootController()
self.pushController(withName: "IKANHAZCHEEZEBURGR", context: nil)
The problem is that there isn't enough time between the calls, and there's no lambda for me to execute a semaphore or push the controller.
If I step through with the debugger, it happens, no problem.
If I just hit "run," it no work.
This is what is known as a "heisenbug".
I guess I could do a one-shot timer, but that seems to be such a hideous hack that it may actually cause a disruption of The Force.
Any better ideas? What am I missing?
I know there's a TON of answers for iOS. They don't do me a whole lot of good, here.
Well, I succumbed to The Dark Side, and did the timer hack. It works. I need to give it around 0.4 seconds per open controller.
Here's an approximation of what I did:
self.popToRootController()
let _ = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: false)
func timerCallback(_ timer: Timer) {
if let timerIndex = timer.userInfo as? Int {
if 0 <= timerIndex {
DispatchQueue.main.async {self.pushController(withName: "IKANHAZCHEEZEBURGR", context: nil)}
}
}
}
UPDATE: I do want to mention that, even though this "solves" my issue, the issue that this issue is even an issue is an issue. My design was bad, and I am redesigning the basic navigation. I'll be using a page-based approach, instead of this hierarchical design.
On general principle, if I need to hack to make it work, I'm usually better off doing it a different way.

Timing accuracy with swift using GCD dispatch_after

I'm trying to create a metronome for iOS in Swift. I'm using a GCD dispatch queue to time an AVAudioPlayer. The variable machineDelay is being used to time the player, but its running slower than the time I'm asking of it.
For example, if I ask for a delay of 1sec, it plays at 1.2sec. 0.749sec plays at about 0.92sec, and 0.5sec plays at about 0.652sec. I could try to compensate by adjusting for this discrepancy but I feel like there's something I'm missing here.
If there's a better way to do this altogether, please give suggestions. This is my first personal project so I welcome ideas.
Here are the various functions that should apply to this question:
func milliseconds(beats: Int) -> Double {
let ms = (60 / Double(beats))
return ms
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
if self.playState == false {
return
}
playerPlay(playerTick, delay: NSTimeInterval(milliseconds(bpm)))
}
func playerPlay(player: AVAudioPlayer, delay: NSTimeInterval) {
let machineDelay: Int64 = Int64((delay - player.duration) * Double(NSEC_PER_SEC))
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, machineDelay),dispatch_get_main_queue(), { () -> Void in
player.play()
})
}
I have never really done anything with sound on iOS but I can tell you why you are getting those inconsistent timings.
What happens when you use dispatch_after() is that some timer is set somewhere in the OS and at some point soon after it expires, it puts your block on the queue. "at some point after" is going to be short, but depending on what the OS is doing, it will almost certainly not be close to zero.
The main queue is serviced by the main thread using the run loop. This means your task to play the sound is competing for use of the CPU with all the UI functionality. This means that the chance of it playing the sound immediately is quite low.
Finally, the completion handler will fire at some short time after the sound finishes playing but not necessarily straight away.
All of these little delays add up to the latency you are seeing. Unfortunately, depending on what the device is doing, that latency can vary. This is never going to work for something that needs precise timings.
There are, I think, a couple of ways to achieve what you want. However, audio programming is beyond my area of expertise. You probably want to start by looking at Core Audio. My five minutes of research suggests either Audio Queue Services or OpenAL, but those five minutes are literally everything I know about sound on iOS.
dispatch_after is not intended for sample accurate callbacks.
If you are writing audio applications there is no way to escape, you need to implement some CoreAudio code in one way or another.
It will "pull" specific counts of samples. Do the math (figuratively ;)

Resources