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.
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.")
})
}
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.")
})
}
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()
}
}
}
Details
- Xcode 10.2.1 (10E1001), Swift 5
Solution
import Foundation
class SimultaneousOperationsQueue {
typealias CompleteClosure = ()->()
private let dispatchQueue: DispatchQueue
private lazy var tasksCompletionQueue = DispatchQueue.main
private let semaphore: DispatchSemaphore
var whenCompleteAll: (()->())?
private lazy var numberOfPendingActionsSemaphore = DispatchSemaphore(value: 1)
private lazy var _numberOfPendingActions = 0
var numberOfPendingTasks: Int {
get {
numberOfPendingActionsSemaphore.wait()
defer { numberOfPendingActionsSemaphore.signal() }
return _numberOfPendingActions
}
set(value) {
numberOfPendingActionsSemaphore.wait()
defer { numberOfPendingActionsSemaphore.signal() }
_numberOfPendingActions = value
}
}
init(numberOfSimultaneousActions: Int, dispatchQueueLabel: String) {
dispatchQueue = DispatchQueue(label: dispatchQueueLabel)
semaphore = DispatchSemaphore(value: numberOfSimultaneousActions)
}
func run(closure: ((@escaping CompleteClosure) -> Void)?) {
numberOfPendingTasks += 1
dispatchQueue.async { [weak self] in
guard let self = self,
let closure = closure else { return }
self.semaphore.wait()
closure {
defer { self.semaphore.signal() }
self.numberOfPendingTasks -= 1
if self.numberOfPendingTasks == 0, let closure = self.whenCompleteAll {
self.tasksCompletionQueue.async { closure() }
}
}
}
}
func run(closure: (() -> Void)?) {
numberOfPendingTasks += 1
dispatchQueue.async { [weak self] in
guard let self = self,
let closure = closure else { return }
self.semaphore.wait(); defer { self.semaphore.signal() }
closure()
self.numberOfPendingTasks -= 1
if self.numberOfPendingTasks == 0, let closure = self.whenCompleteAll {
self.tasksCompletionQueue.async { closure() }
}
}
}
}
Usage
let queue = SimultaneousOperationsQueue(numberOfSimultaneousActions: 1, dispatchQueueLabel: "AnyString")
queue.whenCompleteAll = { print("All Done") }
// add task with sync/async code
queue.run { completeClosure in
// your code here...
// Make signal that this closure finished
completeClosure()
}
// add task only with sync code
queue.run {
// your code here...
}
Full sample
import UIKit
class ViewController: UIViewController {
private lazy var queue = { SimultaneousOperationsQueue(numberOfSimultaneousActions: 1,
dispatchQueueLabel: "AnyString") }()
private weak var button: UIButton!
private weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 50, y: 80, width: 100, height: 100))
button.setTitleColor(.blue, for: .normal)
button.titleLabel?.numberOfLines = 0
view.addSubview(button)
self.button = button
let label = UILabel(frame: CGRect(x: 180, y: 50, width: 100, height: 100))
label.text = ""
label.numberOfLines = 0
label.textAlignment = .natural
view.addSubview(label)
self.label = label
queue.whenCompleteAll = { [weak self] in self?.label.text = "All tasks completed" }
//sample1()
sample2()
}
func sample1() {
button.setTitle("Run 2 task", for: .normal)
button.addTarget(self, action: #selector(sample1Action), for: .touchUpInside)
}
func sample2() {
button.setTitle("Run 10 tasks", for: .normal)
button.addTarget(self, action: #selector(sample2Action), for: .touchUpInside)
}
private func add2Tasks() {
queue.run { completeTask in
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds(1)) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.label.text = "pending tasks \(self.queue.numberOfPendingTasks)"
}
completeTask()
}
}
queue.run {
sleep(1)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.label.text = "pending tasks \(self.queue.numberOfPendingTasks)"
}
}
}
@objc func sample1Action() {
label.text = "pending tasks \(queue.numberOfPendingTasks)"
add2Tasks()
}
@objc func sample2Action() {
label.text = "pending tasks \(queue.numberOfPendingTasks)"
for _ in 0..<5 { add2Tasks() }
}
}
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.
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...
Using swift 5:
Try to run this code in playground. You can play with ordered and unordered results
let dispatchGroup:DispatchGroup = DispatchGroup()
let semaphore = DispatchSemaphore(value: 1)
let globalQueue = DispatchQueue(label: "globalQueue")
let innerQueue = DispatchQueue(label: "innerQueue")
func doLongThing(i:Int, completion:@escaping ()->Void){
print("In \(i)")
innerQueue.sync {
sleep(2)
print("Out \(i)")
completion()
}
}
globalQueue.async {
for i in 0..<4{
dispatchGroup.enter()
doLongThing(i: i) {
print("Finished \(i)")
semaphore.signal()
}
semaphore.wait()
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
print("ALL Done")
}
}
print("In the meantime ...")
We can do this with recursion. Get idea from below code :
var count = 0
func uploadImages(){
if count < viewModel.uploadImageModelArray.count {
let item = viewModel.uploadImageModelArray[count]
self.viewModel.uploadImageExpense(filePath: item.imagePath, docType: "image/png", fileName: item.fileName ?? "", title: item.imageName ?? "", notes: item.notes ?? "", location: item.location ?? "") { (status) in
if status ?? false {
// successfully uploaded
}else{
// failed
}
self.count += 1
self.uploadImages()
}
}
}
Dispatch group is good but the order of sent requests is random.
Finished request 1
Finished request 0
Finished request 2
In my project case, each requests needed to be launch is the right order. If this could help someone :
public class RequestItem: NSObject {
public var urlToCall: String = ""
public var method: HTTPMethod = .get
public var params: [String: String] = [:]
public var headers: [String: String] = [:]
}
public func trySendRequestsNotSent (trySendRequestsNotSentCompletionHandler: @escaping ([Error]) -> () = { _ in }) {
// If there is requests
if !requestItemsToSend.isEmpty {
let requestItemsToSendCopy = requestItemsToSend
NSLog("Send list started")
launchRequestsInOrder(requestItemsToSendCopy, 0, [], launchRequestsInOrderCompletionBlock: { index, errors in
trySendRequestsNotSentCompletionHandler(errors)
})
}
else {
trySendRequestsNotSentCompletionHandler([])
}
}
private func launchRequestsInOrder (_ requestItemsToSend: [RequestItem], _ index: Int, _ errors: [Error], launchRequestsInOrderCompletionBlock: @escaping (_ index: Int, _ errors: [Error] ) -> Void) {
executeRequest(requestItemsToSend, index, errors, executeRequestCompletionBlock: { currentIndex, errors in
if currentIndex < requestItemsToSend.count {
// We didn't reach last request, launch next request
self.launchRequestsInOrder(requestItemsToSend, currentIndex, errors, launchRequestsInOrderCompletionBlock: { index, errors in
launchRequestsInOrderCompletionBlock(currentIndex, errors)
})
}
else {
// We parse and send all requests
NSLog("Send list finished")
launchRequestsInOrderCompletionBlock(currentIndex, errors)
}
})
}
private func executeRequest (_ requestItemsToSend: [RequestItem], _ index: Int, _ errors: [Error], executeRequestCompletionBlock: @escaping (_ index: Int, _ errors: [Error]) -> Void) {
NSLog("Send request %d", index)
Alamofire.request(requestItemsToSend[index].urlToCall, method: requestItemsToSend[index].method, parameters: requestItemsToSend[index].params, headers: requestItemsToSend[index].headers).responseJSON { response in
var errors: [Error] = errors
switch response.result {
case .success:
// Request sended successfully, we can remove it from not sended request array
self.requestItemsToSend.remove(at: index)
break
case .failure:
// Still not send we append arror
errors.append(response.result.error!)
break
}
NSLog("Receive request %d", index)
executeRequestCompletionBlock(index+1, errors)
}
}
Call :
trySendRequestsNotSent()
Result :
Send list started
Send request 0
Receive request 0
Send request 1
Receive request 1
Send request 2
Receive request 2
...
Send list finished
See for more infos : Gist
来源:https://stackoverflow.com/questions/35906568/wait-until-swift-for-loop-with-asynchronous-network-requests-finishes-executing