admin管理员组

文章数量:1125611

// 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?

// 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?

Share Improve this question asked Jan 9 at 6:53 CCCCCCCC 6,4219 gold badges58 silver badges141 bronze badges 4
  • 1 You should not have nested ObservableObject, there are many SO posts dealing with this subject, search for them. Consider using the more modern approach using @Observable class instead. – workingdog support Ukraine Commented Jan 9 at 7:33
  • In SwiftUI the View structs are the view model and to use async/await it is .task. – malhal Commented 2 days ago
  • Why are you nesting view models? View models are 1:1 they should never speak to each other – lorem ipsum Commented 2 days ago
  • stackoverflow.com/questions/78023270/… – lorem ipsum Commented 2 days ago
Add a comment  | 

3 Answers 3

Reset to default 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

There are a couple of issues in your code.

The obvious one, which belongs to "How to use SwiftUI", is utilising "@StateObject vs @ObservableObject":

A common pattern is, "Define a data model as an observable object, then instantiate the model in a view as a state object (@StateObject), and then passes the instance to a subview as an observed object (@ObservableObject)."

Note, that this pattern more closely resembles the Model in the MVVM pattern, rather than the ViewModel!

For that, I would recommend reading the official documentation ObservedObject first, which succinctly describes this pattern. Then, there is an additional wealth of information when you search for "@StateObject vs @ObservableObject" in the Internet.

Then, I would recommend to refine the pattern a bit. Let's make the Model event driven, and let it publish its data:

final class Model: ObservableObject {
    enum Event {
        case requestData
    }
     
    @Published var data: [Device] = []

    func send(event: Event) {
       ... // perform the logic
    }

    // we might also have an internal "state":
    // private var state: State
}
struct ModelView: View {
    @StateObject private var model = Model()

    var body: some View {
        ChildView(
            state: model.data, 
            onAppear: { model.send(.requestData) },
            onUpdateIntent: { model.send(.requestData) }
        )
    }
}

Now, in order to nicely solve this use case, you need a better design. There's the question, how to better reflect the behaviour and all its possible "states". I recommend starting with imagine what a user could possible see on the screen, and what actions a user can take in the View, in all imaginable scenarios, and express this in a data structure:

So, what can a user see in all possible scenarios:

  • an empty view, i.e. no content
  • no content, and it's currently loading
  • some content
  • some content, and its currently loading
  • no content and an error alert
  • some content and an error alert

What can a user possible do (user events, aka intents):

  • explicitly tap a button to load/update the content
  • confirm an error alert
  • perform pull-to-refresh
  • cancel a loading operation

There are also other actors, so ask them too:

What can the view actively do (view events):

  • send an event when appearing

What can the model actively do (model events):

  • send an event when data is there

The above describes, what your use case is about. You now compose the various state and values into a "ViewState", and all possible events into an "Event" data type:

enum Content {
    case none(NoContent) 
    case some([Device])
}

enum ViewState {
   case idle(Content) 
   case loading(Content)
   case error(Error, content: Content)
}

Sum up the events (or actions):

enum Event {
    case viewDidAppear
    case updateContent
    case refreshContent
    case confirmErrorAlert
    case modelResponse(Result<[Device], Error>)
}

The "Thing" that now computes the ViewState value can be denoted the "ViewModel" - however, we might not want to stick too strictly to this notion, it's more important to know, what it actually does:


final class ViewModel: ObservableObject {
   @Published private(set) var viewState: ViewState 

   func send(_ event: Event) {
       update(&viewState, event: event)
   }

   private func update(_ state: inout ViewState, event: Event) {
       switch (state, event) {
          case (.idle(.none), .viewDidAppear): 
              ...
       }
   }
}

(The above is more similar to a "Model" in the MVI pattern, than a ViewModel in the MVVM pattern.)

This design allows you to define a ViewState and actions (aka events). which clearly describe the behaviour and the visual components of the use case - in plain data. That means, even knowing nothing, how an actual view might render it, but due to being able to easily "see" how the screen looks like and behaves, it is completely sufficient and easy to find the correct logic. This will be implemented in the update function, and only in this function.

There's also no need for two-way-bindings, and I would strongly discourage you to use them, because this only complicates the implementation. The design above is strictly unidirectional and event-driven.

With a little more refinement, the "update" can be made a pure function, which adds another very strong principle to the design, making it resilient, easy to reason about and enhances the chances to get it correct and complete with a few lines of code.

The details of this design surely need a bit more explanation. I would recommend trying it out, and see how far you can get. ;)

Update

An intriguing idea is, that we can also implement this kind of ViewModel completely as a SwiftUI view! Note, that a SwiftUI view is not a view ;)

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 { // runs on appear, cancelled on dissapear
            do {
               let devices = try await networkManager.fetchDevices()
               result = .success(devices)
            }
            catch { // cancellation or failure
                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.

本文标签: iosSwiftUIStateObject in parent view is unavailable in child view modelStack Overflow