9
\$\begingroup\$

I put together an asynchronous map function for Swift's Array using Grand Central Dispatch. It's called with a transform closure, a dispatch_queue_t (optional), and a completion closure. The transform closure is passed an item from the array and a callback closure which should be called with the result of the transform. When the completion closure is called, it will be passed a new array with the transformed items in the same order as the original array.

Simple example:

Array(1...10).map({
    (num, callback) in
    // Just to show the transform can perform an asynchronous action
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
        callback(num * 2)
    }
}) {
    results in
    println(results)
}

Outputs:

[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

By default, the transform closure is dispatched to the DISPATCH_QUEUE_PRIORITY_LOW global queue for each item, but a different queue can be passed in if need be.

I'm looking for:

  1. A general code review
  2. Anything that could take more advantage of Swift or GCD

extension Array {
    // Convenience function which passes the global DISPATCH_QUEUE_PRIORITY_LOW queue as the transformQueue
    public func map<U>(transform: (T, U -> ()) -> (), withCompletionHandler completionHandler: [U] -> ()) {
        let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)
        map(transform, onQueue: queue, withCompletionHandler: completionHandler)
    }

    public func map<U>(transform: (T, U -> ()) -> (), onQueue tranformQueue: dispatch_queue_t, withCompletionHandler completionHandler: [U] -> ()) {
        let transformGroup = dispatch_group_create()
        let dataSyncQueue = dispatch_queue_create("array.asyncMap.dataSync", DISPATCH_QUEUE_SERIAL)
        var results: [U?] = [U?](count: count, repeatedValue: nil)

        // Nested function to dispatch the transform; mainly here to capture index from the for loop below
        func performTransform(index: Int) {
            let item = self[index]
            dispatch_group_enter(transformGroup)
            dispatch_async(tranformQueue) {
                transform(item) {
                    result in
                    dispatch_sync(dataSyncQueue) {
                        results[index] = result
                    }
                    dispatch_group_leave(transformGroup)
                }
            }
        }

        for index in (0..<count) {
            performTransform(index)
        }

        dispatch_group_notify(transformGroup, tranformQueue) {
            var unwrappedResults: [U]? = nil

            // This dispatch_sync doesn't seem to be technically needed since all the transforms should have finished at this point
            dispatch_sync(dataSyncQueue) {
                // Force unwrap the values in the results array
                unwrappedResults = results.map({ item in item! })
            }

            completionHandler(unwrappedResults!)
        }
    }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I updated the code to the latest Swift syntax. However, now that we have Combine, doing this is far easier. I wouldn't call this function map because that has a definite meaning that doesn't apply here.


extension Array {
    public func map<U>(transform: @escaping (Element, (U) -> Void) -> Void, onQueue tranformQueue: DispatchQueue = DispatchQueue.global(qos: .utility), withCompletionHandler completionHandler: @escaping ([U]) -> Void) {
        let transformGroup = DispatchGroup()
        let dataSyncQueue = DispatchQueue.init(label: "array.asyncMap.dataSync")
        var results: [U?] = [U?].init(repeating: nil, count: count)

        // Nested function to dispatch the transform; mainly here to capture index from the for loop below
        func performTransform(index: Int) {
            let item = self[index]
            transformGroup.enter()
            tranformQueue.async {
                transform(item) { result in
                    dataSyncQueue.async {
                        results[index] = result
                    }
                    transformGroup.leave()
                }
            }
        }

        for index in (0..<count) {
            performTransform(index: index)
        }
        transformGroup.notify(queue: tranformQueue) {
            var unwrappedResults: [U]? = nil

            // This dispatch_sync doesn't seem to be technically needed since all the transforms should have finished at this point
            dataSyncQueue.async {
                // Force unwrap the values in the results array
                unwrappedResults = results.map({ item in item! })
            }

            completionHandler(unwrappedResults!)
        }
    }
}

Here is how it would be done with Combine:

extension Array {
    func apply<U>(_ transform: (Element) -> AnyPublisher<U, Never>) -> AnyPublisher<[U], Never> {
        reduce(Empty<[U], Never>().eraseToAnyPublisher()) { partialResult, element in
            partialResult.zip(transform(element))
                .map { $0 + [$1] }
                .eraseToAnyPublisher()
        }
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Just changing syntax, without identifying the good and bad things in the code, doesn't really qualify as a review. Is there something you could comment on (perhaps explain why you think the newer syntax improves the code, perhaps? Enough to outweigh incompatibility with older compilers?) \$\endgroup\$ Commented Apr 21, 2022 at 12:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.