How to jump from unsafe to actor code?

I don't use Swift much anymore, but I need to write some adapter code to call Apple's StoreKit from another language. I thought I would use an actor with async methods, since that seems to place nice with the StoreKit API. But how do I jump from global non-concurrency-aware extern "C" world, to the actor?

var storeHelp: StoreHelper? = nil  // error: not concurrency-safe 

@_cdecl("create_store_help_actor")
func create_store_help_actor() {
    storeHelp = StoreHelper()
}

actor StoreHelper {
    var products: [String: Product] = [:]
    func requestProducts(ids: [String]) async { ... }
    func listenForTxns() async { ... }
    ...
}

@_cdecl("listen_for_txns")
func listen_for_txns() {
    // How to get actor here?

According to AI, I can do this:

final class Context: @unchecked Sendable {
    static let shared = Context()
    let storeHelp = StoreHelper()
}
func startUsingIt() {
    Task {
        await Context.shared.storeHelp.foo()
    }
}

I'll do that for now until I learn something better.

I might suggest a very minor refinement, namely make init private:

final class Context: @unchecked Sendable {
    static let shared = Context()

    let storeHelp = StoreHelper()

    private init() { }
}

That will avoid ever accidentally invoking Context() in your code. It doesn’t make too much difference in this example, but it’s the standard defensive-programming of a singleton.


The other approach is to retire Context altogether:

actor StoreHelper {
    static let shared = StoreHelper()

    private init() { }

    func foo() async {…}
}

And then your function is just:

func startUsingIt() {
    Task {
        await StoreHelper.shared.foo()
    }
}
2 Likes

You may consider using a completion handler to indicate when the task is completed and when startUsingInit() truly completes vs. just when the task is created.

    func startDoingIt(completion: @escaping () -> Void) {
        Task {
            await StoreHelper.shared.foo()
            completion()
        }
    }