Home > Enterprise >  Swift Concurrency - how to make a closure call part of async code
Swift Concurrency - how to make a closure call part of async code

Time:06-16

I have something like this in my code and I was wondering if it is possible to make the compiler happy with the closures that I have:

struct Configuration {
    var acceptAction: () -> Void
    var denyAction: () -> Void
}

func someExample() async throws -> String {
    doSomething(
        configuration: Configuration(
            acceptAction: {
                // call async method fetchSomething that returns a string
            }, denyAction: {
                // throw
            }
        )
    )
}

func doSomething(configuration: Configuration) {
    // one of the closures will be called here
}

func fetchSomething() async throws -> String {
    //
}

Any ideas are welcome.

CodePudding user response:

Before we get to your example, let us first consider a simpler example. If you are trying to wrap a closure-based completion handler in an async method, you would use withCheckedThrowingContinuation. E.g.

func bar(completion: @escaping (Result<String, Error>) -> Void) {
    // ...
}

You could call that using withCheckedThrowingContinuation:

func baz() async throws -> String {
    try await withCheckedThrowingContinuation { continuation in
        bar { result in
            switch result {
            case .failure(let error):  continuation.resume(throwing: error)
            case .success(let string): continuation.resume(returning: string)
            }
        }
    }
}

So, using your example, it might be:

func someExample() async throws -> String {
    try await withCheckedThrowingContinuation { continuation in
        doSomething(
            configuration: Configuration {
                Task {
                    do {
                        let string = try await self.fetchSomething()
                        continuation.resume(returning: string)
                    } catch {
                        continuation.resume(throwing: error)
                    }
                }
            } denyAction: {
                continuation.resume(throwing: ExampleError.denied)
            }
        )
    }
}

Generally, I would expect the denyAction to pass the error as a parameter (and I would use that), but in this case, you didn't supply any specific error explaining why it was denied, so I just made one:

enum ExampleError: Error {
    case denied
}

As an aside, Xcode 13.4 has refactoring tools to simplify the process a little. For example, consider a traditional Result-based completion handler closure. Xcode can recognize automatically that it has an error type, and will generate the template with withCheckedThrowingContinuation:

enter code here


See WWDC 2021 video Swift concurrency: Update a sample app for examples of how to wrap methods with completion handler closures into Swift concurrency. It shows the use of these refactoring tools, too.

CodePudding user response:

You could do something like this. This is a way to start an unstructured task.

func someExample() async throws -> String {
    doSomething(
        configuration: Configuration(
            acceptAction: {
                // call async method fetchSomething that returns a string
                let _ = Task {
                    try await fetchSomething()
                }
                
            }, denyAction: {
                // throw
            }
        )
    )
}

Here is another example:

func fetchData(completion: @MainActor @escaping (Result<[UIImage], Error>) -> Void) {
    URLSession.shared.dataTask(with: URL(string: "..some URL")!) { data, response, error in
        // .. Decode data to a result
        let result: Result<[UIImage], Error> = .success([])
        
        Task {
            await completion(result)
        }
    }
}
  • Related