How to wait for a function call inside a loop

I'm banging my head trying to figure this out and I feel like I am missing something here. I am trying to illeterate through a loop in order and then send a request to php to update my database. The problem is the code keeps inserting the lines at random.


Here is the latest I have tried which I found online to try and use DispatchGroup, in my viewcontroller I have this function


    private let myGroup = DispatchGroup()
    
    @IBAction func submitBtn(_ sender: Any) {
        if GlobalVars.isSearching { return }
        print("Sending SQL Update for Work Order \(lblWorkOrder.text ?? "") ")
        
        //Convert string to html style for Spectrum
        let existingLines = txtNotes.text.components(separatedBy: CharacterSet.newlines)
        
        txtNotes.text = ""
        for (index, element) in existingLines.enumerated() {
            myGroup.enter()
            
            let newln = "<p>\(element)</p>"
            self.txtNotes.text += newln
            
            var final = String(0)
            var partial = String(1)
            if (index == element.count - 1) {
                final = String(1)
            } else if index == 0 {
                partial = String(0)
            }
            
            workOrders.sqlUpdateWorkorder(self.lblEquipment.text, WO: self.lblWorkOrder.text, Notes: newln, Meter: self.txtMeter.text, User: UIDevice.current.name, Type: self.Unit?.meterType, Partial: partial, Final: final, Line: String(index))
        }
    }

which calls to this delegate function


    func sqlUpdateWorkorder(_ Unit: String?, WO: String?, Notes: String?, Meter: String?, User: String?, Type: String?, Partial: String?, Final: String?, Line: String?) {
        var components = URLComponents(string: GlobalVars.phpFile)
        let param1 = URLQueryItem(name: "cmd", value: "UpdateWorkOrder")
        let param2 = URLQueryItem(name: "unit", value: Unit)
        let param3 = URLQueryItem(name: "wo", value: WO)
        let param4 = URLQueryItem(name: "notes", value: Notes)
        let param5 = URLQueryItem(name: "meter", value: Meter)
        let param6 = URLQueryItem(name: "user", value: User)
        let param7 = URLQueryItem(name: "type", value: Type)
        let param8 = URLQueryItem(name: "part", value: Partial)
        components?.queryItems = [param1, param2, param3, param4, param5, param6, param7, param8]

        guard let url: URL = (components?.url) else {
            print("Failed to get url from components.")
            return
        }
        
        GlobalVars.submitOnlineQuery(url: url, bgtask: false) { data, error in
            if error != nil {
                self.delegate?.workOrderUpdated(Success: "0")
            } else {
                guard let data = data else {
                    GlobalVars.showAlert(msg: "Error getting data, no data returned from server.", title: "Error!", vc: GlobalVars.topViewController()!, dismiss: true)
                    print("Error: No data to decode")
                    return
                }
                
                let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))
                if Final == "1" {
                    self.delegate?.workOrderUpdated(Success: responseString ?? "0")
                } else {
                    self.delegate?.workOrderUpdated(Success: "Part \(Line ?? "0")")
                }
            }
         }
    }

Which calls to a global function


    static func submitOnlineQuery(url: URL, bgtask: Bool, completionHandler: @escaping (Data?, Error?) -> Swift.Void) {
        GlobalVars.isSearching = true

        if ( !bgtask ) {
            // add the spinner view controller
            let child = SpinnerViewController()
            child.view.frame = (self.topViewController()?.view.frame)!
            self.topViewController()?.view.addSubview(child.view)
            
            // add slow connection label if needed
            let myLabel = connectionLabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
            let task = DispatchWorkItem {
                myLabel.blink()
                self.topViewController()?.view.addSubview(myLabel)
            }
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: task)
            
            guard GlobalVars.isOnline == true else {
                GlobalVars.showAlert(msg: "No internet connection, please try again.", title: "Error: No Network!", vc: (self.topViewController())!, dismiss: false)
                self.stopSpinner(child: child, label: myLabel, task: task)
                completionHandler(nil, myError.noConnection)
                return
            }
            
            // try to download data as requested
            URLSession.shared.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    print("Failed to download data")
                    self.stopSpinner(child: child, label: myLabel, task: task)
                    completionHandler(data, error)
                }else {
                    guard let data = data else {
                        print("Error: No data to decode")
                        self.stopSpinner(child: child, label: myLabel, task: task)
                        completionHandler(nil, myError.noData)
                        return
                    }
                    
                    print("Successfully queried URL")
                    self.stopSpinner(child: child, label: myLabel, task: task)
                    completionHandler(data, error)
                }
                }.resume()
        } else {
            DispatchQueue.global(qos: .background).async {
                // try to download data as requested
                URLSession.shared.dataTask(with: url) { (data, response, error) in
                    if error != nil {
                        print("BGTask => Failed to download data")
                        GlobalVars.isSearching = false
                        completionHandler(data, error)
                    }else {
                        guard let data = data else {
                            print("BGTask => Error: No data to decode")
                            GlobalVars.isSearching = false
                            completionHandler(nil, myError.noData)
                            return
                        }
                        
                        print("BGTask => Successfully queried URL")
                        GlobalVars.isSearching = false
                        completionHandler(data, error)
                    }
                    }.resume()
            }
        }

Then upon retreival in my viewcontroller this is my delegate return


    func workOrderUpdated(Success: String) {
        myGroup.leave()
        if (Success.contains("Part")) {
            print("Successfully added \(Success)")
        } else {
            GlobalVars.showAlert(msg: Success, title: "Update Status", vc: self, dismiss: true)
        }
    }


This is the result


Sending SQL Update for Work Order 11835 
Successfully queried URL
Successfully added Part 0
Successfully queried URL
Successfully added Part 4
Successfully queried URL
Successfully added Part 1
Successfully queried URL
Successfully added Part 3
Successfully queried URL
Successfully added Part 7
Successfully queried URL
Successfully added Part 8
Successfully queried URL
Successfully added Part 9
Successfully queried URL
Successfully added Part 5
Successfully queried URL
Successfully added Part 6
Successfully queried URL
Successfully added Part 2

The problem is the code keeps inserting the lines at random.

I’d like to clarify what this means. Is your specific concern that the lines are get inserted in a random order?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Queries are sent async, in parallel. If the server respond faster, you'll get it before.


Can't you reorder at the end, or when you receive the answer ?


Could you also complete the print with:


print("BGTask => Successfully queried URL", url)

Yes sorry, the lines are being sent to the php server in order but the replies are being returned in random, depending on the time it takes to return. As per the last part of the post, the Successfully added Part ### is the actual line number being returned.

I'm not sure what you mean by reorder at the end. This is basically using php to update an sql database. I guess my real question is, how can I send a command to the php server, then wait for the return before sending the next line. I will add the print suggestion to review as you suggested.

Your right, they are all being sent in order and received different depending on response time.


Sending SQL Update for Work Order 11835 
Sending query for URL 0
Sending query for URL 1
Sending query for URL 2
Sending query for URL 3
Sending query for URL 4
Sending query for URL 5
Sending query for URL 6
Sending query for URL 7
Sending query for URL 8
BGTask => Successfully queried URL 0
Successfully added Part 0
BGTask => Successfully queried URL 3
Successfully added Part 3
BGTask => Successfully queried URL 5
Successfully added Part 5
BGTask => Successfully queried URL 1
Successfully added Part 1
BGTask => Successfully queried URL 2
Successfully added Part 2
BGTask => Successfully queried URL 6
Successfully added Part 6
BGTask => Successfully queried URL 4
Successfully added Part 4
BGTask => Successfully queried URL 8
Successfully added Part 8
BGTask => Successfully queried URL 7
Successfully added Part 7


How can I send for query 0 then wait until returned before sending query 1, remembering this is done in a loop function to start.

I got around it by adding a globalvar and then releasing it upon data return from the server


            while GlobalVars.isSearching {}
            GlobalVars.isSearching = true


It works, but I was thinking I could make the function wait. I still feel there is a much better way to do this but I am very new to programming and swift.

When you make a request, you will always get s response, which you should be checking. If the HTTP status code is valid (say 200, 201, or 204) then you can kick off the next request in the completion handler. You can setup a little structure to describe the expected interaction and use that to give your program a more iterative architecture.

That's one thing you should never do. Or MUST NOT may be a good term.

This line:

while GlobalVars.isSearching {}

consumes all the CPU core power just for waiting and makes other tasks blocked.

Never do it. Your app would be a battery eater with very bad response.


(Using too many globale variables, is another thing you should not do, but that's another issue...)


Generally, when working with asynchronous operations, there's one good principle


NEVER wait.


In your case, you should trigger a new operation when the previous operation is finished.

import UIKit

class ViewController: UIViewController {
    
    
    var pendingQueries = [URL]()
    var isFreeToMakeMoreQueries = true{
        didSet{
             guard isFreeToMakeMoreQueries == true else { return } // Edited: I forgot to add this line
            if !pendingQueries.isEmpty{
                 let url = pendingQueries.removeFirst()
                
                  isFreeToMakeMoreQueries = false
                
                GlobalVars.submitOnlineQuery(url: url, bgtask: false) { data, error in
      
                    if error != nil {
                        self.delegate?.workOrderUpdated(Success: "0")
                    } else {
                        guard let data = data else {
                            GlobalVars.showAlert(msg: "Error getting data, no data returned from server.", title: "Error!", vc: GlobalVars.topViewController()!, dismiss: true)
                            print("Error: No data to decode")
                            return
                        }
                        
                        let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))
                        if Final == "1" {
                            self.delegate?.workOrderUpdated(Success: responseString ?? "0")
                        } else {
                            self.delegate?.workOrderUpdated(Success: "Part \(Line ?? "0")")
                        }
                    }
                }
            }
           
        }
    }

    private let myGroup = DispatchGroup()
    
    @IBAction func submitBtn(_ sender: Any) {
        if GlobalVars.isSearching { return }
        print("Sending SQL Update for Work Order \(lblWorkOrder.text ?? "") ")
        
        //Convert string to html style for Spectrum
        let existingLines = txtNotes.text.components(separatedBy: CharacterSet.newlines)
        
        txtNotes.text = ""
        for (index, element) in existingLines.enumerated() {
            myGroup.enter()
            
            let newln = "\(element)
"
            self.txtNotes.text += newln
            
            var final = String(0)
            var partial = String(1)
            if (index == element.count - 1) {
                final = String(1)
            } else if index == 0 {
                partial = String(0)
            }
            
            workOrders.sqlUpdateWorkorder(self.lblEquipment.text, WO: self.lblWorkOrder.text, Notes: newln, Meter: self.txtMeter.text, User: UIDevice.current.name, Type: self.Unit?.meterType, Partial: partial, Final: final, Line: String(index))
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    func sqlUpdateWorkorder(_ Unit: String?, WO: String?, Notes: String?, Meter: String?, User: String?, Type: String?, Partial: String?, Final: String?, Line: String?) {
        var components = URLComponents(string: GlobalVars.phpFile)
        let param1 = URLQueryItem(name: "cmd", value: "UpdateWorkOrder")
        let param2 = URLQueryItem(name: "unit", value: Unit)
        let param3 = URLQueryItem(name: "wo", value: WO)
        let param4 = URLQueryItem(name: "notes", value: Notes)
        let param5 = URLQueryItem(name: "meter", value: Meter)
        let param6 = URLQueryItem(name: "user", value: User)
        let param7 = URLQueryItem(name: "type", value: Type)
        let param8 = URLQueryItem(name: "part", value: Partial)
        components?.queryItems = [param1, param2, param3, param4, param5, param6, param7, param8]
        
        guard let url: URL = (components?.url) else {
            print("Failed to get url from components.")
            return
        }
        
        pendingQueries.append(url)
    }
    
    static func submitOnlineQuery(url: URL, bgtask: Bool, completionHandler: @escaping (Data?, Error?) -> Swift.Void) {
        GlobalVars.isSearching = true
        
        if ( !bgtask ) {
            // add the spinner view controller
            let child = SpinnerViewController()
            child.view.frame = (self.topViewController()?.view.frame)!
            self.topViewController()?.view.addSubview(child.view)
            
            // add slow connection label if needed
            let myLabel = connectionLabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
            let task = DispatchWorkItem {
                myLabel.blink()
                self.topViewController()?.view.addSubview(myLabel)
            }
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: task)
            
            guard GlobalVars.isOnline == true else {
                GlobalVars.showAlert(msg: "No internet connection, please try again.", title: "Error: No Network!", vc: (self.topViewController())!, dismiss: false)
                self.stopSpinner(child: child, label: myLabel, task: task)
                completionHandler(nil, myError.noConnection)
                return
            }
            
            // try to download data as requested
            URLSession.shared.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    print("Failed to download data")
                    self.stopSpinner(child: child, label: myLabel, task: task)
                    completionHandler(data, error)
                }else {
                    guard let data = data else {
                        print("Error: No data to decode")
                        self.stopSpinner(child: child, label: myLabel, task: task)
                        completionHandler(nil, myError.noData)
                        return
                    }
                    
                    print("Successfully queried URL")
                    self.stopSpinner(child: child, label: myLabel, task: task)
                    completionHandler(data, error)
                    
                    isFreeToMakeMoreQueries = true
                }
                }.resume()
        } else {
            DispatchQueue.global(qos: .background).async {
                // try to download data as requested
                URLSession.shared.dataTask(with: url) { (data, response, error) in
                    if error != nil {
                        print("BGTask => Failed to download data")
                        GlobalVars.isSearching = false
                        completionHandler(data, error)
                    }else {
                        guard let data = data else {
                            print("BGTask => Error: No data to decode")
                            GlobalVars.isSearching = false
                            completionHandler(nil, myError.noData)
                            return
                        }
                        
                        print("BGTask => Successfully queried URL")
                        GlobalVars.isSearching = false
                        completionHandler(data, error)
                        
                         isFreeToMakeMoreQueries = true
                    }
                    }.resume()
            }
        }
        
        func workOrderUpdated(Success: String) {
            myGroup.leave()
            if (Success.contains("Part")) {
                print("Successfully added \(Success)")
            } else {
                GlobalVars.showAlert(msg: Success, title: "Update Status", vc: self, dismiss: true)
            }
        }
}

Thats exactly what I want to do but the call is within a loop and I do not know how to trigger the new operation after.

Destruct your loop. There may be many other ways, but my preferred way is using iterator.


Something like this:

    var operations: EnumeratedSequence<[String]>.Iterator?
    var isSearching: Bool = false
    @IBAction func submitBtn(_ sender: Any) {
        if isSearching {
            //It is not a good habit to ignore user's action silently...
            return
        }
        isSearching = true
        print("Sending SQL Update for Work Order \(lblWorkOrder.text ?? "") ")
        
        //Convert string to html style for Spectrum
        let existingLines = txtNotes.text.components(separatedBy: CharacterSet.newlines)
        
        txtNotes.text = ""
        operations = existingLines.enumerated().makeIterator()
        //trigger the first operation
        invokeNextOperation()
    }
    
    private func invokeNextOperation() {
        DispatchQueue.main.async {
            guard let (index, element) = self.operations?.next() else {
                self.operations = nil
                self.isSearching = false
                return
            }
            
            let newln = "<p>\(element)</p>"
            self.txtNotes.text += newln
            
            var final = String(0)
            var partial = String(1)
            if (index == element.count - 1) {
                final = String(1)
            } else if index == 0 {
                partial = String(0)
            }
            
            self.workOrders.sqlUpdateWorkorder(self.lblEquipment.text, WO: self.lblWorkOrder.text, Notes: newln, Meter: self.txtMeter.text, User: UIDevice.current.name, Type: self.Unit?.meterType, Partial: partial, Final: final, Line: String(index))
        }
    }
    
    func workOrderUpdated(Success: String) {
        invokeNextOperation() //<- trigger a new operation when previous operation is completed
        if (Success.contains("Part")) {
            print("Successfully added \(Success)")
        } else {
            GlobalVars.showAlert(msg: Success, title: "Update Status", vc: self, dismiss: true)
        }
    }

Do you control the PHP side of this? If so, my recommendation is that you restructure that to support better on-the-wire behaviour.

It seems like your current plan is something along the lines of:

  1. Send request 1.

  2. When that completes, send request 2.

  3. When that completes, send request 3.

  4. And so on.

This is severely suboptimal from a networking perspective. The problem is latency: Each network request takes a certain amount of time to cross the network, and by serialising your requests your adding these latencies up.

A better approach is:

  1. Send requests 1, 2, 3, and so on.

  2. Wait for them all to complete.

  3. Serialise the responses.

This is what Claude31 was suggesting in their 12 Apr response, and it’s good advice.

If you can’t fix the server, and the requests and responses don’t have metadata that you can use for this serialisation, you can track the request order yourself. The code below shows one way you can do this.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
// Set up a Dispatch group that fires when everything is done, and a
// `responses` array that starts off being populated by placeholders.

let group = DispatchGroup()
var responses = [Result<URLResponse, Error>](repeating: placeholder, count: 10)
group.notify(queue: .main) {
    // … all the tasks are now done …
    // … `responses` contains the responses in the original order …
}

// Create and start a task for each request.  Each tasks enters the
// group before it’s started and leaves the group when it’s done.  When
// all the tasks are done, the group fires.
//
// Before a task leaves the group, it puts its response in the right
// place in the `responses` array.  Thus, when the group fires, the
// `responses` contains all the task responses in the right order.

for i in 0..<responses.count {
    let request = … setup the i’th request …
    let t = self.session.dataTask(with: request) { (data, response, error) in
        DispatchQueue.main.async {
            if let error = error {
                responses[i] = .failure(error)
            } else {
                responses[i] = .success(response!)
            }
            group.leave()
        }
    }
    group.enter()
    t.resume()
}
How to wait for a function call inside a loop
 
 
Q