SwiftUI + MVVM + async tasks in model

Hi,


I’m new to Swift and also to SwiftUI.

For a small application that I want to implement I’d like to stick with MVVM.


But async tasks in the model are giving me a headache.


I have created a very simplified example to explain it:


The View:


import SwiftUI

struct ContentView: View {

    @ObservedObject var viewModel: ViewModel

    var body: some View {
        VStack {
            Text("\(viewModel.model.numberToDisplay)")
            Button(action: {self.viewModel.model.increase()}) {
                Text("increase")
            }
        }
    }

    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView(viewModel: ViewModel())
        }
    }
}


The ViewModel:


import Foundation
import SwiftUI

class ViewModel: ObservableObject {
    @Published var model = Model()
}


And the Model:


import Foundation

struct Model {

    private(set) var numberToDisplay: Int = 0

    mutating func increase() {
        for _ in 0...2 {
            self.numberToDisplay += 1
            sleep(1)
        }
    }
}


Having this the UI will show “0” at the start. Hitting “increase” gives a blocked UI for 3s (see for loop in the model) and afterwards it’ll show “3”.


What I would like to have is a non-blocked UI and that all updates of numberToDisplay are shown (0 -> 1 -> 2 -> 3 instead of 0 -> 3).


If I try to use a DispatchQueue like this in the model:


DispatchQueue.global().async {
     self.numberToDisplay += 1
}


it just gives me an „Escaping closure captures mutating 'self' parameter” error.


Using a class instead of a struct for the model allows me to use a DispatchQueue, but even worse the UI does not get updated at all. (The usage of DispatchQueue does not play any role in here.)


So what the appropriate approach to use SwiftUI with MVVM and async functions within the model?


Any help is appreciated.


Best regards

AlphaPapa

Using a class instead of a struct for the model allows me to use a DispatchQueue, but even worse the UI does not get updated at all. (The usage of DispatchQueue does not play any role in here.)

I don't know whether you should use a struct or class for Model, but if you do use a class, then for the UI to update, there needs to be some connection between the Model changing and the ViewModel firing objectWillChange. That happens without any code if you have a @Published struct property, but with a class it needs something like this... (I didn't test run this code but it's close.)

Code Block swift
class ViewModel: ObservableObject {
var model = Model()
var sub: AnyCancellable?
init() {
sub = model.willChange.sink { self.objectWillChange.send() }
}
}
class Model {
var numberToDisplay: Int = 0 {
willSet { willChange.send() }
}
var willChange = PassthroughSubject<Void,Never>()
func increase() {
numberToDisplay += 1
if numberToDisplay < 5 {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { self.increase() }
}
}
}


Hi AlphaPapa, Swift structs are value types and they aren't allowed to capture 'self' in closures. Setting up your model as a class is probably better for your use case. Using sleep should be avoided, because it blocks the entire thread. You can use asyncAfter instead:

Code Block swift
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
self?.numberToDisplay += 1
}


Notice that the closure runs on the main queue. That is important, because the UI can only be updated from the main thread.
SwiftUI &#43; MVVM &#43; async tasks in model
 
 
Q