I can't update EnvironmentObject with data from network response. In view, initial data are displayed properly. I want class calling to API to update global state with response. Then I got the crash
Fatal error: No ObservableObject of type AppState found.
A View.environmentObject(_:) for AppState may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-30.4/Core/EnvironmentObject.swift, line 55#
struct GameView: View {
var location = LocationService()
#EnvironmentObject var appState: AppState
var body: some View {
VStack{
Text("countryRegion: \(self.appState.countryRegion)")
Text("adminDistrict: \(self.appState.adminDistrict)")
}.onAppear(perform: startMonitoring)
}
func startMonitoring() {
self.appState.isGameActive = true
self.location.startMonitoringLocation()
}
}
class LocationService: NSObject, CLLocationManagerDelegate{
#EnvironmentObject var appState: AppState
...
func getAddress(longitude: CLLocationDegrees, latitude: CLLocationDegrees) {
let url = URL(string: "http://dev.virtualearth.net/REST/v1/Locations/\(latitude),\(longitude)?o=json&key=\(self.key)")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
let response = try! JSONDecoder().decode(Results.self, from: data)
DispatchQueue.main.async {
self.appState.adminDistrict = response.resourceSets[0].resources[0].address.adminDistrict;
self.appState.countryRegion = response.resourceSets[0].resources[0].address.countryRegion;
}
}.resume()
}
AppState declaration:
class AppState: ObservableObject {
#Published var isGameActive = false
#Published var countryRegion = ""
#Published var adminDistrict = ""
}
post the struct that attaches the AppState environment object to the view hierarchy.
I have attached AppState in SceneDelegate to ContentView as my root View (if I get it right)
window.rootViewController = UIHostingController(rootView:ContentView().environmentObject(appState))
Should I attach it to every View that modifies AppState?
Related
I have a simple view that shows some photos, through a list. Clicking on any row should display a detailed view of that photo. I'm using the MVVM pattern. However, an infinite loop occurs when I try to set the “selectedPhoto” property of the view model. Is there any way to avoid this loop without having to create a property in the detailed view itself?
Here is the Photo struct:
struct Photo: Identifiable {
var id = UUID()
var name: String
}
Here is the ContentView with an extension (the “updatePhoto” method is causing the infinite loop):
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.photos) { selectedPhoto in
showDetailView(with: selectedPhoto)
}
}
.navigationTitle("Favorite Photo")
}
}
}
extension ContentView {
func showDetailView(with selectedPhoto: Photo?) -> some View {
if let selectedPhoto = selectedPhoto {
viewModel.updatePhoto(selectedPhoto)
}
return DetailView(viewModel: viewModel)
}
}
Here is the view model:
class ViewModel: ObservableObject {
#Published var photos = [
Photo(name: "Photo 1"),
Photo(name: "Photo 2"),
Photo(name: "Photo 3")
]
#Published var selectedPhoto: Photo?
func updatePhoto(_ selectedPhoto: Photo?) {
self.selectedPhoto = selectedPhoto
}
}
And here is the DetailView:
struct DetailView: View {
#ObservedObject private var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
Text(viewModel.selectedPhoto?.name ?? "Unknown photo name")
}
}
Try this approach, using a NavigationLink to present the DetailView,
and passing the selectedPhoto to it using #State var selectedPhoto: Photo.
struct Photo: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var photos = [Photo(name: "Photo 1"),Photo(name: "Photo 2"),Photo(name: "Photo 3")]
}
struct DetailView: View {
#State var selectedPhoto: Photo
var body: some View {
Text(selectedPhoto.name)
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.photos) { selectedPhoto in
NavigationLink(selectedPhoto.name, destination: DetailView(selectedPhoto: selectedPhoto))
}
}
.navigationTitle("Favorite Photo")
}
}
}
Note that NavigationView is being deprecated and you will have to use NavigationStack instead.
I am working with SwiftUI and am using MVVM with my VM acting as my EnvironmentObjects.
I first create a AuthSession environment object which has a string for currentUserId stored.
I then create another environment object for Offers that is trying to include the AuthSession environment object so I can can filter results pulled from a database with Combine. When I add an #EnvironmentObject property to the Offer view model I get an error stating that AuthSession is not passed. This makes sense since it's not a view.
My question is, is it best to sort the results in the view or is there a way to add an EnvironmentObject to another EnvironmentObject? I know there is an answer here, but this model answer is not using VM as the EOs.
App File
#main
struct The_ExchangeApp: App {
// #EnvironmentObjects
#StateObject private var authListener = AuthSession()
#StateObject private var offerHistoryViewModel = OfferHistoryViewModel(offerRepository: OfferRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(authListener)
.environmentObject(offerHistoryViewModel)
}
}
}
AuthSession.swift
class AuthSession: ObservableObject {
#Published var currentUser: User?
#Published var loggedIn = false
#Published var currentUserUid = ""
// Intitalizer
init() {
self.getCurrentUserUid()
}
}
OfferHistoryViewModel.swift - The error is called just after the .filter in startCombine().
class OfferHistoryViewModel: ObservableObject {
// MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
// Access to AuthSession for filtering offer made by the current user.
#EnvironmentObject var authSession: AuthSession
// Properties
var offerRepository: OfferRepository
// Published Properties
#Published var offerRowViewModels = [OfferRowViewModel]()
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
// Intitalizer
init(offerRepository: OfferRepository) {
self.offerRepository = offerRepository
self.startCombine()
}
// Starting Combine - Filter results for offers created by the current user only.
func startCombine() {
offerRepository
.$offers
.receive(on: RunLoop.main)
.map { offers in
offers
.filter { offer in
(self.authSession.currentUserUid != "" ? offer.userId == self.authSession.currentUserUid : false) // ERROR IS CALLED HERE
}
.map { offer in
OfferRowViewModel(offer: offer)
}
}
.assign(to: \.offerRowViewModels, on: self)
.store(in: &cancellables)
}
}
Error
Thread 1: Fatal error: No ObservableObject of type AuthSession found. A View.environmentObject(_:) for AuthSession may be missing as an ancestor of this view.
I solved this by passing currentUserUid from AuthSession from my view to the view model. The view model changes to the following.
class OfferHistoryViewModel: ObservableObject {
// MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
var offerRepository: OfferRepository
// Published Properties
#Published var offerRowViewModels = [OfferRowViewModel]()
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
// Intitalizer
init(offerRepository: OfferRepository) {
self.offerRepository = offerRepository
}
// Starting Combine - Filter results for offers created by the current user only.
func startCombine(currentUserUid: String) {
offerRepository
.$offers
.receive(on: RunLoop.main)
.map { offers in
offers
.filter { offer in
(currentUserUid != "" ? offer.userId == currentUserUid : false)
}
.map { offer in
OfferRowViewModel(offer: offer)
}
}
.assign(to: \.offerRowViewModels, on: self)
.store(in: &cancellables)
}
}
Then in the view I pass the currentUserUid in onAppear.
struct OfferHistoryView: View {
// MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
#EnvironmentObject var authSession: AuthSession
#EnvironmentObject var offerHistoryViewModel: OfferHistoryViewModel
// MARK: ++++++++++++++++++++++++++++++++++++++ View ++++++++++++++++++++++++++++++++++++++
var body: some View {
// BuildView
} // View
.onAppear(perform: {
self.offerHistoryViewModel.startCombine(currentUserUid: self.authSession.currentUserUid)
})
}
}
This works well for me and I hope it helps someone else.
I am working with SwiftUI and #EnvironmentObjects. I am using the SwiftUI App Lifecycle. In this file I create a #StateObjects for ListingRepository and attach it to ContentView() with .envrionmentObjects().
struct MyApp: App {
// #EnvironmentObjects
#StateObject private var listingRepository = ListingRepository()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(listingRepository)
}
}
}
My assumption was that I could now access listingRepository by using an #EnvironmentObject wrapper. However, it seems I must instantiate this again. Below is my ListingViewModel. My first attempt looked like the following.
class MarketplaceViewModel: ObservableObject {
#EnvironmentObject var listingRepository: ListingRepository
#Published var listingRowViewModels = [ListingRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
This threw the following error.
Fatal error: No ObservableObject of type ListingRepository found. A
View.environmentObject(_:) for ListingRepository may be missing as an
ancestor of this view.
The second option uses #Published and fixes the error.
class MarketplaceViewModel: ObservableObject {
#Published var listingRepository = ListingRepository()
#Published var listingRowViewModels = [ListingRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
My questions are the following.
Is it necessary to instantiate twice like I am?
Why doesn't the instantiation in #main App work?
in your #main App you should define your environment object like this:
struct MyApp: App {
// #EnvironmentObjects
var listingRepository = ListingRepository()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(listingRepository)
}
}
}
Then it suffices to have the ListingRepository define as:
class ListingRepository: ObservableObject {
#Published var ...
#Published var ...
// Your code here
}
Don't redeclare the ListingRepository as an EnvironmentObject in your MarketPlaceViewModel.
One note, however, it is not wise to make your ListingRepository an observable object and access this object from your views through the MarketPlaceViewModel. If the MarketPlaceViewModel is used to fill the views in your app and the MarketPlaceViewModel gets the data from the ListingRepository, you should make your MarketPlaceViewModel the EnvironmentObject and not the ListingRepository.
The thing in SwiftUI is the you want the EnvironmentObject to publish changes to several views in your app, so that these views can reconstruct themselves. If you use a view model between your model and your view, the view model should be the EnvironmentObject.
How to update view, when view models publish var's (user's) , name property is updated. I do know why its happening but what is the best way to update the view in this case.
class User {
var id = "123"
#Published var name = "jhon"
}
class ViewModel : ObservableObject {
#Published var user : User = User()
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
userNameView
}
var userNameView: some View {
Text(viewModel.user.name)
.background(Color.red)
.onTapGesture {
viewModel.user.name += "update"
print( viewModel.user.name)
}
}
}
so one way i do it, is by using onReceive like this,
var body: some View {
userNameView
.onReceive(viewModel.user.$name){ output in
let tmp = viewModel.user
viewModel.user = tmp
print("onTapGesture",output)
}
}
but it is not a good approach it will update all view using users properties.
should i make a #state var for the name?
or should i just make a ObservedObject for user as well?
Make you class conform to ObservableObject
class User: ObservableObject {
var id = "123"
#Published var name = "jhon"
}
But he catch with that is that you have to observe it directly you can't chain it in a ViewModel
Use #ObservedObject var user: User in a View
You should use struct:
import SwiftUI
struct User {
var id: String
var name: String
}
class ViewModel : ObservableObject {
#Published var user : User = User(id: "123", name: "Mike")
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel()
var body: some View {
userNameView
}
var userNameView: some View {
Text(viewModel.user.name)
.background(Color.red)
.onTapGesture {
viewModel.user.name += " update"
print( viewModel.user.name)
}
}
}
I am getting the error
Fatal error: No ObservableObject of type LoginResponse found. A View.environmentObject(_:) for LoginResponse may be missing as an ancestor of this view.
I have passed the environment object to all the subviews of LoginView(), but the error still persists
struct LoginView: View {
#State var loginCredentials = Credentials()
var loginResponse = LoginResponse()
var dataCall = DataCall()
var body: some View {
VStack{
Button(action:{self.dataCall.AttemptLogin(loginCredentials: self.loginCredentials)}){
Text("LOGIN")
}
Text("\(self.loginResponse.jwt)")
}
.environmentObject(self.loginResponse)
.padding(.horizontal)
}
}
Here is the rest of the code if needed
struct Credentials: Encodable {
var login: String = ""
var password: String = ""
}
class LoginResponse: Decodable, ObservableObject {
enum CodingKeys: CodingKey {
case success, userId, jwt
}
#Published var success: Bool = false
#Published var userId: String = ""
var error: String = ""
#Published var jwt: String = ""
init(){}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
success = try values.decode(Bool.self, forKey: .success)
userId = try values.decode(String.self, forKey: .userId)
jwt = try values.decode(String.self, forKey: .jwt)
}
}
struct DataCall {
#EnvironmentObject var dataResponse : LoginResponse
func AttemptLogin(loginCredentials: Credentials) {
let loginResponse = LoginResponse()
dataResponse.success = loginResponse.success
}
}
Since DataCall is not a SwiftUI view, I don't believe you can use the .environmentObject modifier to pass data to it. Instead, just add a reference to a LoginResponse object in the DataCall struct, and pass it through the constructor like normal.
Example:
struct DataCall {
weak var dataResponse : LoginResponse
...
}
let dataCall = DataCall(dataResponse: loginResponse)
However, I'm pretty confused about your view design. Any object that you declare normally in a View, without #State or #ObservedObject, will be recreated over and over again. Is that what you want?