Skip to main content
7 of 9
a caveat
Milos
  • 619
  • 5
  • 18

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
    }
}
Milos
  • 619
  • 5
  • 18