The proposed approach hangs both on iOS and macOS (just not in mac targeted unit tests), which is fascinating in its own right. See inline comments:
@main
struct BlockerApp: App {
@State var count = 0
var body: some Scene {
WindowGroup {
VStack {
Text("\(count)")
Button("Increment") { // tapping on the button hangs the app
count += blocking { 1 } // the closure is never called
}
}
}
}
}
func blocking<A>(_ ƒ: @escaping () async -> A) -> A {
let semafore = DispatchSemaphore(value: 0)
let got = Got<A>()
Task.detached(priority: .high) {
got.a = await ƒ() // gets as far as this line but ƒ is never called
semafore.signal()
}
semafore.wait()
return got.a!
}
private class Got<A> {
var a: A?
}
However, if we require that the closure is executed in another context, for example by annotating the parameter with a custom global actor, then this solution works on both macOS and iOS:
@globalActor public actor BlockingActor {
public static let shared = BlockingActor()
}
@available(*, deprecated, message: "For use only to allow incremental migration to structured concurrency")
public func blocking<A>(_ ƒ: @BlockingActor @escaping () async -> A) -> A {
let semaphore = DispatchSemaphore(value: 0)
let got = Got<A>()
Task(priority: .high) {
got.a = await ƒ()
semaphore.signal()
}
semaphore.wait()
return got.a!
}
Alas, a nested call would still cause a hang:
count += blocking {
blocking { // moving to the same executor, so we hang as before
1
}
}