If I'm understanding the question correctly, this is what an actor is for. Try this:
import UIKit
actor MyActor {
var running = false
func doYourTimeConsumingThing() async throws {
guard !running else { print("oh no you don't"); return }
running = true
print("starting at", Date.now.timeIntervalSince1970)
try await Task.sleep(nanoseconds: 5_000_000_000) // real task goes here
print("finished at", Date.now.timeIntervalSince1970)
running = false
}
}
class ViewController: UIViewController {
let actor = MyActor()
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
Task { [weak self] in
try? await self?.actor.doYourTimeConsumingThing()
}
}
}
}
As you'll see, the timer tries to start the task every second, but if the task is running, the attempt is turned back; you can only start the task if it isn't already running. The actor makes all this perfectly safe and thread-coherent.
With regard to your comment:
The thing missing is, that if the timeConsumingThing is called while running, I still need the result eventually... Ideally a second call would just „subscribe“ to the same running async Task.
I think we can emulate this by adding an actual publish-and-subscribe to the mix. First, let me separate out the actual task and make it return a result; this is supposed to be your WeatherKit interaction:
func timeConsumingTaskWithResult() async throws -> Date {
try await Task.sleep(nanoseconds: 5_000_000_000)
return Date.now
}
Now I'll revise the actor slightly so that new callers are forced to wait for the next result to come back from the latest WeatherKit interaction:
actor MyActor {
var running = false
@Published var latestResult: Date?
func doYourTimeConsumingThing() async throws -> Date? {
if !running {
running = true
latestResult = try await timeConsumingTaskWithResult()
running = false
}
for await result in $latestResult.values {
return result
}
fatalError("shut up please, compiler")
}
}
Finally, the test bed is much as before, but now I'm getting a result for the call made on each firing of the timer, and I'll print it when I get it:
class ViewController: UIViewController {
let actor = MyActor()
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
Task { [weak self] in
print("calling at", Date.now)
if let result = try? await self?.actor.doYourTimeConsumingThing() {
print("RESULT!", result)
}
}
}
}
}
That yields:
calling at 2022-08-28 15:35:39 +0000
calling at 2022-08-28 15:35:40 +0000
calling at 2022-08-28 15:35:41 +0000
calling at 2022-08-28 15:35:42 +0000
calling at 2022-08-28 15:35:43 +0000
calling at 2022-08-28 15:35:44 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:46 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:47 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:48 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:49 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:50 +0000
RESULT! 2022-08-28 15:35:45 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:51 +0000
calling at 2022-08-28 15:35:52 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:53 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:54 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:55 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:56 +0000
RESULT! 2022-08-28 15:35:50 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:35:58 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:35:59 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:36:00 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:36:01 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:36:02 +0000
RESULT! 2022-08-28 15:35:57 +0000
RESULT! 2022-08-28 15:36:02 +0000
calling at 2022-08-28 15:36:03 +0000
As you can see, someone calls into our actor every second. Every caller eventually gets a result, and they are all the same result, 2022-08-28 15:35:45
, because that is the time when the time-consuming task returned. From then on, the more recent callers all start getting 2022-08-28 15:35:50
, because that is the time when the next time-consuming task returned. The call to the time consuming task, as in my earlier example, is gated so that it cannot be called until it has returned from its previous call.
try await
directly. – Preindicatetry await alreadyRunningTask
that task is triggered again instead of reusing it, right? – ClarygetTimeline
(so I can't control how often it is called). In my full code I do react to errors by simply showing placeholders in the widget (nothing else one can do really if there is no data). Widgets are static, so there is no user input. All I can do is try to get the desired information and display it. – ClarygetTimeline
is only called once. The fact that xcode adds asystemSmall
widget to my homescreen when running my scheme should have tipped me off. I do still find the question I posted quite interesting from a theoretical point of view though. How can I prevent tasks from running multiple times in parallel if I call an async function from different parts of my app at roughly the same time? – Clary