SwiftUI: Update UI after background thread has finished

Viewed 1647

In my app I import a JSON file and display its content in the app. In order to avoid freezing of the UI, because the file can be bigger sometimes, I parse the file in a background thread and convert it into my internal swift structure. When the parsing is done, the view should be redrawn, as the content of the structure has changed. Because I mustn't change the view from a background thread I have some kind of workaround at the moment. I start the parsing in the action of the .fileImporter modifier of a button like this:

DispatchQueue.global(qos: .userInitiated).async {
    documentManager.parseInput()
}

And in the parseInput method in the documentManager I do something like this

func parseInput() {
    DispatchQueue.main.async { [self] in
        inputIsParsed = false
    }
    
    // parse ...
            
    DispatchQueue.main.async { [self] in
        inputIsParsed = true
    }
}

inputIsParsed is a @Published variable in the documentManager, which is my ViewModel

 @Published var inputIsParsed = false

My question now is: Is this as bad as I think it is or is there no better way that probably also involves a completely different concept of putting work into the background and redrawing the view when the calculation has finished?

3 Answers

You should use a completion block closure or delegate to know when data is loaded and then return to main thread to update UI.

func loadData(completion: @escaping(Data?) -> Void) {
    // Parse your data here instead
    DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
        completion(Data())
    }
}
    
func loadDataAndUpdateUI() {
    loadData { data in
        DispatchQueue.main.async {
            // update ui
        }
    }
}

I am doing the same thing, running some stuff on background threads and then updating the UI via async operation on the main thread I could not find any other way. To make this process easier, I am using a function that makes sure the operation is running on the main thread. I find it as something that makes my code simpler since many times we are running inside async functions that we are not sure whether they are in the main thread or another background thread (suppose you use some 3rd party API and it invokes an async callback - you cannot be sure where your code is running now)

So I am using this global function:

func RunInMainThread(execute: @escaping ()->Void) {
    if Thread.isMainThread {
         execute()
    }
    else {
        DispatchQueue.main.async {
            execute()
        }
    }
}

To use it, just do something like:

RunInMainThread {
    inputIsParsed = true
}

I think better method is to use PassThroughSubject that doesn't store it anywhere just publishes the change.

in your ViewModel you define:

var currentAction = PassthroughSubject<Action, Never>()

you also define enum Action:

enum Action {
    case inputParsed
    case anythingElseToReport
}

the advantage is you get to have greater flexibility. And you can observe it anywhere via:

var anyCancellables: Set<AnyCancellable> = []

ViewModel.currentAction
        .sink { value in                
            switch value { //You can also use if value == Action.inputParsed
            case .inputParsed: return //Your action
            default: return
            }
        }
        .store(in: &anyCancellables)

PS: In SwiftUI views you use .onReceive() on the view to get the action instead of .sink.

Related