I'd like to understand how timed events work in ios/swift. For example, let's say that I want to write a music metronome and I want to trigger a sound in each beat. I understand that there are frameworks like AudioKit, but my question is about timed functions that would help me.
A metronome is a good example because we should have a "tick" in a precise manner.
import Foundation
let bpm = 60
let timer = Timer.scheduledTimer(withTimeInterval: Double(bpm/60), repeats: true) { timer in
// do stuff
}
// you can also cancel the timer
timer.invalidate()
But keep in mind that timer might not be the most precise I am assuming audio kit has more percise abstractions to work with this kind of problems.
Timer also only works on the main roon loop (main thread).
You rarely work with threads directly when doing iOS. I suggest you read about Grand Central Dispatch which is used a lot when doing iOS and dispatch queues are an abstraction used for performing asynchronous operations.
You mean Timer.scheduledTimer(withTimeInterval:repeats:block:) Runs in .main, too?
I was following the example At Hacking with Swift with Timer.publish(). The timer drive data update, which in turn cause lots of animations. My app was very unresponsive to touch/swipe. I was sure it’s due to the timer runs on: .main
So what's the different between Timer.publish() vs. Timer.scheduledTimer()? If they both run on .main, then there should NOT be any difference either way, but my app runs better using Timer.scheduledTimer().
I tried Timer.publish(..., on: .init(), ...). Did not work. Nothing change, like no data update.
This thread starts to evolve into a framework question. It'd feel more at home in the Apple Dev Forum. Anyway, here's some documentation about RunLoop.
In general, each thread has a single RunLoop object, retrieved using .current. Sending events to a run loop does nothing, you need to also tell the run loop to handle it using a variation of run() command.
The main thread is a little special that the OS triggers run for you. Now here's the caveat, you should only put user-interactive things like UI update in the main run loop. More importantly, refrain from putting blocking code there, otherwise, it could block the actual UI commands, making the app feels unresponsive even if there's plenty of CPU resources available.
The .scheduleTimer(withTimeInterval:repeats:block:) specifically says to run on the current run loop. So unless you're triggering this from a separated non-ui thread, it should be on the main runloop. I don't know what you did to the published function. Since there's also subscription mechanism involved that I'm not familiar with. Though I don't think it should really affect the performance that much.
This also explain why .published(..., on: .init(), ...) does nothing. You're sending event to newly created, thus unhandled, runloop (Though it's weird that you can actually create a runloop this way).
Problem is both iOS and SwiftUI are private Apple frameworks and operating systems, and policy has been that there are other resources for getting help with the proprietary Apple software.
I would like to say, that (DispatchQueue.main.asyncAfter(deadline:..) very depends on device which it works, so time intervals can vary greatly. The most stable way which works almost same on all devices is the function Date(). It shows same dates in same times.