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 |3 Answers
Reset to default 1I 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
版权声明:本文标题:ios - SwiftUI - StateObject in parent view is unavailable in child view model - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736667377a1946734.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
@Observable class
instead. – workingdog support Ukraine Commented Jan 9 at 7:33View
structs are the view model and to use async/await it is.task
. – malhal Commented 2 days ago