How to await async JS function inside Swift using JavaScriptCore and Callback

684 Views Asked by At

I would like to call an async JS function from within Swift using JavaScriptCore. However, when the JS function is asynchronous, how do I force the Swift function call to wait until the async javascript function behind it has completed running?

Currently all this call returns is

[object Promise]

This is my current attempt


import SwiftUI
import JavaScriptCore

class Cube {
    func getNfts(pubKey: String) async -> String? {
        let jsCode =
    """
    async function lookupNfts(pubKey, callback) {
            try {
            const config = {
              apiKey: "API KEY",
              network: Network.MATIC_MUMBAI,
            };
            const alchemy = new alc.Alchemy(config);
            const { nfts } = await fetch(alchemy.nft.getNftsForOwner(pubKey));
            callback(nfts.ownedNfts)
            return nfts.ownedNfts
                //swiftCallback(nfts);
            } catch (error) {
                console.log(error);
            }
        };
    """
        let context = JSContext()
        context!.evaluateScript(jsCode)
    
        guard let alc = Bundle.main.path(forResource: "alchemy", ofType: "js") else {
            print("Unable to read resource file")
            return nil
        }
        
        let functionInJS = context!.objectForKeyedSubscript("lookupNfts")
        let _ = print(functionInJS)
        let testBlock : @convention(block) (JSValue?) -> Void = { calledBackValue in
            let _ = print("calledBackValue:", calledBackValue)
        }
        let callback = JSValue.init(object: testBlock, in: context)
        
        let res = functionInJS!.call(withArguments: [pubKey, callback]).toString()
        let _ = print(res)
        return res
    }
}
struct ContentView: View {
   var myvar = "hello";
   @State private var sourceCode = "Loading…"

    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            //Cube().getNfts(pubKey:  "wallet address")
            Text(sourceCode ?? "No result")
            })
        }
        .padding()
        .task{
            let mycube = Cube()
            var res = "";
            do {
                let data = await mycube.getNfts(pubKey:  "mypubkey") ?? "nothinghere"
                sourceCode = String(data)
            } catch {
                sourceCode = "Failed to fetch site."
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

(absolute swift newcomer)

1

There are 1 best solutions below

0
iUrii On

Javascript async functions operates with Promise under the hood and you can invoke then method on your [object Promise] with fulfilment and rejection handlers so you don't need any additional callbacks to get a resolved values or rejected errors, e.g.:

context.evaluateScript(
"""
async function lookupNfts(pubKey) {
    if (pubKey) {
        return [1, 2, 3];
    }
    else {
        throw "Key is empty"
    }
}
""")

let onFulfilled: @convention(block) (JSValue) -> Void = {
    print($0)
}
let onRejected: @convention(block) (JSValue) -> Void = {
    print($0)
}
let promiseArgs = [unsafeBitCast(onFulfilled, to: JSValue.self), unsafeBitCast(onRejected, to: JSValue.self)]

let lookupNfts = context.objectForKeyedSubscript("lookupNfts")

lookupNfts?.call(withArguments: ["some-key"])
    .invokeMethod("then", withArguments: promiseArgs)
// Prints: 1,2,3

lookupNfts?.call(withArguments: [])
    .invokeMethod("then", withArguments: promiseArgs)
// Prints: Key is empty

Based on this we can implement a generic swift async function which waits for the Promise's handlers:

extension JSContext {
    func callAsyncFunction(key: String, withArguments: [Any] = []) async throws -> JSValue {
        try await withCheckedThrowingContinuation { continuation in
            let onFulfilled: @convention(block) (JSValue) -> Void = {
                continuation.resume(returning: $0)
            }
            let onRejected: @convention(block) (JSValue) -> Void = {
                let error = NSError(domain: key, code: 0, userInfo: [NSLocalizedDescriptionKey : "\($0)"])
                continuation.resume(throwing: error)
            }
            let promiseArgs = [unsafeBitCast(onFulfilled, to: JSValue.self), unsafeBitCast(onRejected, to: JSValue.self)]
            
            let promise = self.objectForKeyedSubscript(key).call(withArguments: withArguments)
            promise?.invokeMethod("then", withArguments: promiseArgs)
        }
    }
}

How to use:

Task {
    do {
        var data = try await context.callAsyncFunction(key: "lookupNfts", withArguments: ["some-key"])
        print(data) // Prints: 1,2,3
        
        data = try await context.callAsyncFunction(key: "lookupNfts")
    }
    catch {
        print(error.localizedDescription) // Prints: Key is empty
    }
}