Temporarily switch from background thread to main thread (and back?)

I've got a function that runs on a background thread and downloads files:

func downloadFiles(remoteFolder rf:String, localFolder lf:String, completion: @escaping (_ success:Bool, _ err: String) -> Void) {
    DispatchQueue.global(qos:.background).async {
        ...
        DispatchQueue.main.async {
            completion(successBool, errorMsg)
        }
    }
}

It's called by my main ViewController:

myDownloadClass.downloadFiles(remoteFolder: rf, localFolder: lf, completion: { (success, error) in
    ... //More stuff here
}

Is there a way to temporarily switch back to the main thread (and with that also to the VC) in downloadFiles, so I can add the name of the file that's next in the download list to an e.g. UIAlertController but without tripping off everything that should be done after downloading everything actually finished ("More stuff here")?

I'm using Xcode 13 with Swift 5 and iOS 13.

Replies

Could you explain more what you want ?

You download files. When do you want to switch to the main thread ? Each time a file is downloaded ?

Did you consider dispatch groups ?

Also may have a look here https://stackoverflow.com/questions/24056205/how-to-use-background-thread-in-swift

  • Atm it's set up to download every file in a folder. First I get the folder content as a list, then I loop through all the files and download them. At the beginning of each loop iteration the file name is taken from the list and I want to somehow pass it back to the main/UI thread, so I can display an alert with an info what file is currently being downloaded. So: 1. Create alert, 2. Switch to background thread (see function above), 3. Get list, 4. Get file name, 5. Update alert with file name, 6. Download file, 7. Repeat 4.-6. until all files are downloaded, 8. Switch back to main thread (see function above) and do more stuff (including hiding the alert again) but only when everything's done.

  • I just read through the whole stackoverflow link you posted and there's nothing about switching back only temporarily. There's DispatchQueue.main.async, which switches back to the main thread but doesn't give you a way to specify a function in the ViewController and completion switches back permanently because, well, it's a completion handler. Of course I could give the download class a reference to the AlertController (then use DispatchQueue.main.async) but I'm not sure passing around UI elements like that is a good solution.

Add a Comment

The first thing that comes to mind would be using a notification.

func downloadFiles(remoteFolder rf:String, localFolder lf:String, completion: @escaping (_ success:Bool, _ err: String) -> Void) {
    DispatchQueue.global(qos:.background).async {
        let fileName:String = (use whatever setter you have from your loop)
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "updateDownloadFileName", object: nil, userInfo: ["fileName":fileName]))
        ...
        DispatchQueue.main.async {
            completion(successBool, errorMsg)
        }
    }
}

In your ViewController:

let myLabel = NSTextField(labelWithString: "")

override func viewDidLoad() {
    setupNotificationCenter()
}

func updateDownloadFileName(notification: Notification) {
    if let data = notification.userInfo as? [String:String] {
        if let string = data["fileName"] {
            myDownloadLabel.stringValue = string
        }
    }
}

func setupNotificationCenter() {
    NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "updateDownloadFileName"), object: nil, queue: nil, using: updateDownloadFileName)
}

If you don't want to use Notifications then you could pass the Label to the method and update the stringValue through a dispatch to the main thread:

func downloadFiles(remoteFolder rf:String, localFolder lf:String, myLabel:NSTextField, completion: @escaping (_ success:Bool, _ err: String) -> Void) {
    DispatchQueue.global(qos:.background).async {
        let fileName:String = (use whatever setter you have from your loop)
        DispatchQueue.main.async {
            myLabel.stringValue = fileName
        }
        ...
        DispatchQueue.main.async {
            completion(successBool, errorMsg)
        }
    }
}