0

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?

Tim Fuqua
  • 1,635
  • 1
  • 16
  • 25
  • Relevant discussion [here](https://stackoverflow.com/questions/39569114/swift-3-0-error-escaping-closures-can-only-capture-inout-parameters-explicitly). [Edit: It's an older thread, but you might at least take a look and see if it applies.] – scg Dec 09 '20 at 00:09
  • Yep. Already read through that one. But being it was Swift 2 -> Swift 3 I wasn’t sure of the validity. – Tim Fuqua Dec 09 '20 at 00:19

1 Answers1

3

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]
}

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • This was the direction I was going to head in. Not Combine specifically, but some sort of Observable pattern. I need to support iOS 12 still. But I’ll look into your solution nonetheless. – Tim Fuqua Dec 09 '20 at 00:37
  • @TimFuqua, i added a non-Combine approach – New Dev Dec 09 '20 at 00:52
  • DispatchGroup was actually what I was leaning towards. This will probably be the solution I go with. I’ll check it out in a bit. – Tim Fuqua Dec 09 '20 at 00:53