skip to Main Content
// In my root view aka Parent View
import SwiftUI

struct RootView: View {
    @StateObject private var devices = DevicesViewModel()
    
    var body: some View {
        ScrollView {
            UnitStatusCard(devicesVM: devices)
        }
        .onAppear() {
            devices.fetchDevices()
        }
    }
}

// DeviceViewModel.swift - Parent View Model
import Foundation

class DevicesViewModel: ObservableObject {
    @Published var models: [Device] = []
    
    private let networkManager = NetworkManager<[Device]>()
    
    init() {
        fetchDevices()
    }
    
    public func fetchDevices() {
        Task {
            do {
                if let unitId = UserDefaults.standard.string(forKey: kUnit) {
                    let models = try await networkManager.fetchData(path: "/api/test")

                    DispatchQueue.main.async {
                        self.models = models
                    }
                }
            } catch {...}
        }
    }
}

// UnitStatusCard.swift - Child View
struct UnitStatusCard: View {
     @StateObject var unitStatusCardVM: UnitStatusCardViewModel
    
    init(devicesVM: DevicesViewModel) {
        self._unitStatusCardVM = StateObject(wrappedValue: UnitStatusCardViewModel(devicesVM: devicesVM))
    }
    
    var body: some View {
        StatusView()
            .onAppear() {
                unitStatusCardVM.getStatusMeta()
            }
    }
}

// UnitStatusCardViewModel.swift - Child View Model
class UnitStatusCardViewModel: ObservableObject {
     @Published var value: String = "Good"
    
     var devicesVM: DevicesViewModel
    
    init(devicesVM: DevicesViewModel) {
        self.devicesVM = devicesVM
    }
    
    public func getStatusMeta() {
        print(devicesVM.models) // value is [], WHY??
    }
}

In DeviceViewModel.swift, there is a Api call, the result is fetched succesfully without error.
However, when I pass the result to my child view model (UnitStatusCardViewModel), the value is empty even it’s correctly fetched according to ProxyMan.

    public func getStatusMeta() {
        print(devicesVM.models) // value is [], WHY??
    }

Why is that and how to fix it?

2

Answers


  1. I think there are several things that may lead to this result. The fetchDevices method is performing an asynchronous operations inside (although is not marked async, it executes a Task and seems to perform a sort of network call. Depending on when you check the model the value gets printed before the fetch (eg. the getStatusMeta is called on onAppear, likely before the fetch to complete his inner execution.

    I would recommend not running the fetchDevices in the DeviceViewModel init method, it’s anyway called in the onAppear of the RootView, and this means that it get called twice, and plus, eventual actions leading to an ui change at the creation of an object, before or while it gets attached to a Swift UI view, may lead to undesirable result in terms of ui threading (those infamous violet/purple warnings that may show up in Xcode during development)

    one more thing: In vision of being stricted concurrency check compliant, it is strongly reccomended to not use the old fashioned DispatchAsync as you did

    DispatchQueue.main.async {
        self.models = models
    }
    

    rather change it in

    await MainActor.run {
        self.models = … // make sure Device is Sendable or marked as @unchecked Sendable
    }
    

    or even mark
    the view model as @MainActor may be an alternative, so any change on the DeviceViewModel properties will be isolated and executed intrinsically on main thread

    Login or Signup to reply.
  2. In SwiftUI the View structs are the view model and to use async/await it is .task, e.g.

    struct RootView: View {
        @Environment(.networkManager) var networkManager
        @State var result: Result<[Device], Error>?
        
        var body: some View {
            ScrollView {
                if case let result = .success(let devices) {
                    UnitStatusCard(devices: devices)
                }
            }
            .task {
                do {
                   let devices = await networkManager.fetchDevices()
                   result = .success(devices)
                }
                catch {
                    result = .failure(error)
                } 
            }
        }
    }
    

    NetworkManager should be a struct that holds the async funcs and its in the Environment so it can be mocked for Previews.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search