Search code examples
multithreadingswiftuiuikitworkerlong-running-processes

Swift: How to put worker into separate thread, show results in main view?


I wonder if there is an easy way to do this: Put a long-running worker into a separate thread so as not to block the UI. And display the results in the main view. Under SwiftUI or UIKit. What I found on the web was all very complex. Or is there a completely different approach in Swift?

I made a minimalistic Python program to show what I want to do. It displays the WiFi signal strength in MacOS.

import time
import sys
import subprocess
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
from PyQt5.QtCore import QObject, pyqtSignal, QThread

class Worker(QObject):
    send_output = pyqtSignal(str)
    def __init__(self):
        super().__init__()

    def worker(self):
        while True:
            _out = subprocess.check_output(
                ["/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport",
                 "-I"])\
                .decode("utf-8")
            self.send_output.emit("RSSI: " + _out.splitlines()[0][-3:] + " dBm")
            time.sleep(2)

class MainWindow(QWidget):
    do_work = pyqtSignal(object)
    def __init__(self):
        super().__init__()

        self.label = QLabel()
        layout = QHBoxLayout()
        self.setLayout(layout)
        layout.addWidget(self.label)
        self.show()

        self.app_thread = QThread()
        self.app = Worker()
        self.app.moveToThread(self.app_thread)
        self.app.send_output.connect(self.output_label)
        self.app_thread.started.connect(self.app.worker)
        self.app_thread.start()

    def output_label(self, _str):
        self.label.setText(_str)

if __name__ == '__main__':
    application = QApplication(sys.argv)
    mainwindow = MainWindow()
    sys.exit(application.exec())

I'm trying to find my way into Swift right now. It's exciting, but really a big thing. Thanks in advance!


Solution

  • This is a very broad question, so I'm going to answer it rather broadly as well.

    Yes, you can run tasks in separate threads and then return data to the main thread. There are number of ways to do this, a common one being DispatchQueue.

    Let's take an over-simplified example (definitely not meant to be real-world code):

    struct ContentView : View {
        @State var result = ""
        
        var body: some View {
            Text("Result: \(result)")
                .onAppear {
                    DispatchQueue.global(qos: .background).async {
                        //do your long-running code
                        var innerResult = 0
                        for i in 0...1000000 {
                            innerResult += 5
                            print(i)
                            //use DispatchQueue.main.async here if you want to update during the task
                        }
                        
                        //update at the end:
                        DispatchQueue.main.async {
                            result = "Done! \(innerResult)"
                        }
                    }
                }
        }
    }
    

    In this example, when the view appears, a task is run in a background thread, via DispatchQueue. In this case, I'm just taking advantage of the fact that printing to the console is a rather expensive operation if done a million times.

    When it finishes, it dispatches back to the main thread and updates the results in the state variable.

    DispatchQueue is not specific to UIKit or SwiftUI -- it was just easiest to put the demo together using SwiftUI.

    If you were to truly start writing code to do this, you'd want to do some research about how the DispatchQueues work, including which queue to use, whether to create your own, whether you want tasks done serially, etc, but for the purposes of the broad question of can this be done (and how easily), this at least shows the basics.