Tim Fuqua
Tim Fuqua

Reputation: 1705

Capturing values from synchronous and asynchronous closures

Assume a simple struct like this:

struct Foo {
    let value: Int
    let delay: TimeInterval
}

It has 2 functions, each taking a closure as a parameter. One is synchronously called, the other asynchronously after delay:

extension Foo {
    func sync(_ completion: (Int) -> Void) {
        completion(value)
    }
}

extension Foo {
    func async(_ completion: @escaping (Int) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(delay)) {
            completion(self.value)
        }
    }
}

Now assume I have an array of Foo objects:

let foo1 = Foo(value: 1, delay: 1)
let foo2 = Foo(value: 2, delay: 0)

Now I want to query each of them to get the values they would supply and put those into an array. Synchronously, it can be done like this:

let syncValues = [foo1, foo2].reduce(into: []) { values, foo in foo.sync { values.append($0) } } // [1, 2]

Asynchronously, I want to do this and have foo2 report before foo1 does:

let asyncValues = [foo1, foo2].reduce(into: []) { values, foo in foo.async { values.append($0) }  // [2, 1]}

However, I get an error when I try to do the asynchronous version: Escaping closure captures 'inout' parameter 'values'

What is a good way to perform this asynchronous task?

Upvotes: 0

Views: 130

Answers (1)

New Dev
New Dev

Reputation: 49590

Because it's async, you can't do this in a single synchronous statement. Assuming you understand that, using Combine would probably be easiest:

import Combine

let fooResultsPublishers = [foo1, foo2].publisher
    .flatMap { foo in
        Future { promise in
           foo.async { promise(.success($0)) }
        }
    }
    .collect()

To actually get the value, you need to use .sink to act on it asynchronously:

fooResultsPublishers
    .sink {
       print($0) // [2,1]
    }
    // store it in long-living property, like
    // var cancellables: Set<AnyCancellable> = []
    .store(in: &cancellables) 

Without Combine, you'd need to use something like a DispatchGroup:

let group = DispatchGroup()
var values: [Int] = []
[foo1, foo2].forEach { foo in
    group.enter()
    foo.async { values.append($0); group.leave() }
}

group.notify(queue: .main) {
    print(values) // [2,1]
}

Upvotes: 3

Related Questions