I had the same question. In my situation the loop was locking the main thread. It wasn't a simple count from 1 -10, on every iteration two other internal functions were running so the UI was blocked. I needed to run it in the background and then when it finished update the UI on the main thread.
I used a background task from this answer and added a loop to it
1- Create an extension on DispatchQueue and add this static method to it
extension DispatchQueue {
    static func myBackground(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) {
        DispatchQueue.global(qos: .background).async {
            background?()
            if let completion = completion {
                DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
                    completion()
                })
            }
        }
    }
}
2- Call the above method and add your loop inside the first part:
DispatchQueue.myBackground(background: {
    // do something in background like run a loop in the background
    for num in 1..<10 {
        // run function 1
        // run function 2 after function 1 is finished
    }
}, completion:{
    // when background job finished, do something in main thread
})
Another method you can also use is a DispatchGroup
let dispatchGroup = DispatchGroup()
for num in 1..<10 {
    // ** call this to enter the group **
    dispatchGroup.enter()
    // run function 1
    // run function 2 after function 1 is finished and call **dispatchGroup.leave()** to exit the group when function 2 is finished
    // make sure to call **dispatchGroup.leave()** or this will never end
}
dispatchGroup.notify(queue: .global(qos: .background)) {
    DispatchQueue.main.async { [weak self] in
        // when background job finished, do something in main thread
    }
}