Wait until swift for loop with asynchronous networ

2019-01-02 19:17发布

I would like a for in loop to send off a bunch of network requests to firebase, then pass the data to a new view controller once the the method finishes executing. Here is my code:

    var datesArray = [String: AnyObject]()

        for key in locationsArray {       
            let ref = Firebase(url: "http://myfirebase.com/" + "\(key.0)")
            ref.observeSingleEventOfType(.Value, withBlock: { snapshot in

                datesArray["\(key.0)"] = snapshot.value

            })
        }

        //Segue to new view controller here, and pass datesArray once it is complete 

I have a couple concerns. First, how do I wait until the for loop is finished and all the network requests are complete? I can't modify the observeSingleEventOfType function, it is part of the firebase SDK. Also, will I create some sort of race condition by trying to access the datesArray from different iterations of the for loop (hope that makes sense)? I've been reading about GCD and NSOperation but I'm a bit lost as this is the first app I've built.

Note: Locations array is an array containing the keys I need to access in firebase. Also, it's important that the network requests are fired off asynchronously. I just want to wait until ALL the asynchronous requests complete before I pass the datesArray to the next view controller.

7条回答
无色无味的生活
2楼-- · 2019-01-02 19:53

Swift 3: You could also use semaphores on this way. It results very helpful, besides you can keep exact track on when and what processes are completed. This has been extracted from my code:

    //You have to create your own queue or if you need the Default queue
    let persons = persistentContainer.viewContext.persons
    print("How many persons on database: \(persons.count())")
    let numberOfPersons = persons.count()

    for eachPerson in persons{
        queuePersonDetail.async {
            self.getPersonDetailAndSave(personId: eachPerson.personId){person2, error in
                print("Person detail: \(person2?.fullName)")
                //When we get the completionHandler we send the signal
                semaphorePersonDetailAndSave.signal()
            }
        }
    }

    //Here we will wait
    for i in 0..<numberOfPersons{
        semaphorePersonDetailAndSave.wait()
        NSLog("\(i + 1)/\(persons.count()) completed")
    }
    //And here the flow continues...
查看更多
何处买醉
3楼-- · 2019-01-02 19:58

You will need to use semaphores for this purpose.

 //Create the semaphore with count equal to the number of requests that will be made.
let semaphore = dispatch_semaphore_create(locationsArray.count)

        for key in locationsArray {       
            let ref = Firebase(url: "http://myfirebase.com/" + "\(key.0)")
            ref.observeSingleEventOfType(.Value, withBlock: { snapshot in

                datesArray["\(key.0)"] = snapshot.value

               //For each request completed, signal the semaphore
               dispatch_semaphore_signal(semaphore)


            })
        }

       //Wait on the semaphore until all requests are completed
      let timeoutLengthInNanoSeconds: Int64 = 10000000000  //Adjust the timeout to suit your case
      let timeout = dispatch_time(DISPATCH_TIME_NOW, timeoutLengthInNanoSeconds)

      dispatch_semaphore_wait(semaphore, timeout)

     //When you reach here all request would have been completed or timeout would have occurred.
查看更多
何处买醉
4楼-- · 2019-01-02 19:59

You can use dispatch groups to fire an asynchronous callback when all your requests finish.

Here's an example in Swift 4.1 (works in Swift 3 too) using dispatch groups to execute a callback asynchronously when multiple networking requests have all finished.

override func viewDidLoad() {
    super.viewDidLoad()

    let myGroup = DispatchGroup()

    for i in 0 ..< 5 {
        myGroup.enter()

        Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
            print("Finished request \(i)")
            myGroup.leave()
        }
    }

    myGroup.notify(queue: .main) {
        print("Finished all requests.")
    }
}

Output

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.

For those using the older Swift 2.3, here's an example using its syntax:

override func viewDidLoad() {
    super.viewDidLoad()

    let myGroup = dispatch_group_create()

    for i in 0 ..< 5 {
        dispatch_group_enter(myGroup)
        Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
            print("Finished request \(i)")
            dispatch_group_leave(self.myGroup)
        }
    }

    dispatch_group_notify(myGroup, dispatch_get_main_queue(), {
        print("Finished all requests.")
    })
}
查看更多
旧人旧事旧时光
5楼-- · 2019-01-02 20:00

Details

Xcode 9.2, Swift 4

Solution

class AsyncOperation {

    typealias NumberOfPendingActions = Int
    typealias DispatchQueueOfReturningValue = DispatchQueue
    typealias CompleteClosure = ()->()

    private let dispatchQueue: DispatchQueue
    private var semaphore: DispatchSemaphore

    private var numberOfPendingActionsQueue: DispatchQueue
    public private(set) var numberOfPendingActions = 0

    var whenCompleteAll: (()->())?

    init(numberOfSimultaneousActions: Int, dispatchQueueLabel: String) {
        dispatchQueue = DispatchQueue(label: dispatchQueueLabel)
        semaphore = DispatchSemaphore(value: numberOfSimultaneousActions)
        numberOfPendingActionsQueue = DispatchQueue(label: dispatchQueueLabel + "_numberOfPendingActionsQueue")
    }

    func run(closure: @escaping (@escaping CompleteClosure)->()) {

        self.numberOfPendingActionsQueue.sync {
            self.numberOfPendingActions += 1
        }

        dispatchQueue.async {
            self.semaphore.wait()
            closure {
                self.numberOfPendingActionsQueue.sync {
                    self.numberOfPendingActions -= 1
                    if self.numberOfPendingActions == 0 {
                        self.whenCompleteAll?()
                    }
                }
                self.semaphore.signal()
            }
        }
    }
}

Usage

let asyncOperation = AsyncOperation(numberOfSimultaneousActions: 1, dispatchQueueLabel: "AnyString")
asyncOperation.whenCompleteAll = {
    print("All Done")
}
for i in 0...5 {
    print("\(i)")
    asyncOperation.run{ completeClosure in
        // add any (sync/async) code
        //..

        // Make signal that this closure finished
        completeClosure()
    }
}

Full sample

import UIKit

class ViewController: UIViewController {

    let asyncOperation = AsyncOperation(numberOfSimultaneousActions: 3, dispatchQueueLabel: "AnyString")
    let button = UIButton(frame: CGRect(x: 50, y: 80, width: 100, height: 40))
    let label = UILabel(frame: CGRect(x: 180, y: 50, width: 150, height: 100))

    var counter = 1
    var labelCounter = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        button.setTitle("Button", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        label.text = "\(labelCounter)"
        label.numberOfLines = 2
        label.textAlignment = .natural
        view.addSubview(button)
        view.addSubview(label)
    }

    @objc func buttonTapped() {
        //sample1()
        sample2()
    }

    func sample1() {
        print("Sample 1")
        labelCounter += 1
        label.text = "button tapped \(labelCounter) times"

        print("Button tapped at: \(Date())")
        asyncOperation.whenCompleteAll = {
            print("All Done")
        }
        asyncOperation.run{ completeClosure in
            let counter = self.counter
            print("     - Loading action \(counter) strat at \(Date())")
            self.counter += 1

            DispatchQueue.global(qos: .background).async {
                sleep(1)
                print("     - Loading action \(counter) end at \(Date())")
                completeClosure()
            }
        }
    }

    func sample2() {
        print("Sample 2")
        label.text = ""
        asyncOperation.whenCompleteAll = {
            print("All Done")
        }

        for i in 0...5 {
            asyncOperation.run{ completeClosure in
                let counter = self.counter
                print("     - Loading action \(counter) strat at \(Date())")
                self.counter += 1

                DispatchQueue.global(qos: .background).async {
                    sleep(UInt32(i+i))
                    print("     - Loading action \(counter) end at \(Date())")
                    completeClosure()
                }
            }
        }

    }
}

Results

Sample 1

enter image description here

Sample 2

enter image description here

查看更多
孤独总比滥情好
6楼-- · 2019-01-02 20:05

Xcode 8.3.1 - Swift 3

This is the accepted answer of paulvs, converted to Swift 3:

let myGroup = DispatchGroup()

override func viewDidLoad() {
    super.viewDidLoad()

    for i in 0 ..< 5 {
        myGroup.enter()
        Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
            print("Finished request \(i)")
            myGroup.leave()
        }
    }

    myGroup.notify(queue: DispatchQueue.main, execute: {
        print("Finished all requests.")
    })
}
查看更多
与君花间醉酒
7楼-- · 2019-01-02 20:05

Swift 3 or 4

If you don't care about orders, use @paulvs's answer, it works perfectly.

else just in case if anyone wants to get the result in order instead of fire them concurrently, here is the code.

let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "any-label-name")
let dispatchSemaphore = DispatchSemaphore(value: 0)

dispatchQueue.async {

    // use array categories as an example.
    for c in self.categories {

        if let id = c.categoryId {

            dispatchGroup.enter()

            self.downloadProductsByCategory(categoryId: id) { success, data in

                if success, let products = data {

                    self.products.append(products)
                }

                dispatchSemaphore.signal()
                dispatchGroup.leave()
            }

            dispatchSemaphore.wait()
        }
    }
}

dispatchGroup.notify(queue: dispatchQueue) {

    DispatchQueue.main.async {

        self.refreshOrderTable { _ in

            self.productCollectionView.reloadData()
        }
    }
}
查看更多
登录 后发表回答