Why Is EnvironmentObject Not Working in SwiftUI Project? - swiftui

I am working on a SwiftUI Project using MVVM. I have the following files for a marketplace that has listings.
ListingRepository.swift - Connecting to Firebase Firestore
Listing.swift - Listing Model File
MarketplaceViewModel - Marketplace View Model
MarketplaceView - List view of listings for the marketplace
Originally, I was making my repository file the EnvironmentObject which worked. While researching I am realizing it makes more sense to make the ViewModel the EnvironmentObject. However, I am having trouble making an EnvironmentObject. Xcode is giving me the following error in my MarketplaceView.swift file when I try and access marketplaceViewModel and I can't understand why?
SwiftUI:0: Fatal error: No ObservableObject of type
MarketplaceViewModel found. A View.environmentObject(_:) for
MarketplaceViewModel may be missing as an ancestor of this view.
Here are the files in a simplified form.
App File
#main
struct Global_Seafood_ExchangeApp: App {
#StateObject private var authSession = AuthSession()
#StateObject private var marketplaceViewModel = MarketplaceViewModel(listingRepository: ListingRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(marketplaceViewModel)
.environmentObject(authSession)
}
}
}
ListingRepository.swift
class ListingRepository: ObservableObject {
let db = Firestore.firestore()
#Published var listings = [Listing]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
db.collection(FirestoreCollection.listings).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Listings.")
return
}
self.listings = documents.compactMap { listing in
do {
return try listing.data(as: Listing.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
Listing.swift
struct Listing: Codable, Identifiable {
#DocumentID var id: String?
var title: String?
}
MarketplaceModelView.swift
class MarketplaceViewModel: ObservableObject {
var listingRepository: ListingRepository
#Published var listingRowViewModels = [ListingRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init(listingRepository: ListingRepository) {
self.listingRepository = listingRepository
self.startCombine()
}
func startCombine() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
MarketplaceView.swift
struct MarketplaceView: View {
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
// ERROR IS HERE
Text(self.marketplaceViewModel.listingRowViewModels[1].listing.title)
}
}
ListingRowViewModel.swift
class ListingRowViewModel: ObservableObject {
var id: String = ""
#Published var listing: Listing
private var cancellables = Set<AnyCancellable>()
init(listing: Listing) {
self.listing = listing
$listing
.receive(on: RunLoop.main)
.compactMap { listing in
listing.id
}
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
}
ContentView.swift
struct ContentView: View {
#EnvironmentObject var authSession: AuthSession
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
Group{
if (authSession.currentUser != nil) {
TabView {
MarketplaceView()
.tabItem {
Image(systemName: "shippingbox")
Text("Marketplace")
}.tag(0) // MarketplaceView
AccountView(user: testUser1)
.tabItem {
Image(systemName: "person")
Text("Account")
}.tag(2) // AccountView
} // TabView
.accentColor(.white)
} else if (authSession.currentUser == nil) {
AuthView()
}
}// Group
.onAppear(perform: authenticationListener)
}
// MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
func authenticationListener() {
// Setup Authentication Listener
authSession.listen()
}
}
Any help would be greatly appreciated.

in your app you have:
ContentView().environmentObject(marketplaceViewModel)
so in "ContentView" you should have as the first line:
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
Note in "ContentView" you have, "#EnvironmentObject var authSession: AuthSession"
but this is not passed in from your App.
Edit: test passing "marketplaceViewModel", using this limited setup.
class MarketplaceViewModel: ObservableObject {
...
let showMiki = "here is Miki Mouse"
...
}
and
struct MarketplaceView: View {
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
// ERROR NOT HERE
Text(marketplaceViewModel.showMiki)
// Text(self.marketplaceViewModel.listingRowViewModels[1].listing.title)
}
}

Anyone looking for a way to use MVVM with Firebase Firestore and make your View Model the EnvironmentObject I've added my code below. This project has a list view and a detail view. Each view has a corresponding view model. The project also uses a repository and uses Combine.
App.swift
import SwiftUI
import Firebase
#main
struct MVVMTestApp: App {
#StateObject private var marketplaceViewModel = MarketplaceViewModel(listingRepository: ListingRepository())
// Firebase
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(marketplaceViewModel)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Group {
MarketplaceView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
MarketplaceView.swift
import SwiftUI
struct MarketplaceView: View {
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
NavigationView {
List {
ForEach(self.marketplaceViewModel.listingRowViewModels, id: \.id) { listingRowViewModel in
NavigationLink(destination: ListingDetailView(listingDetailViewModel: ListingDetailViewModel(listing: listingRowViewModel.listing))) {
ListingRowView(listingRowViewModel: listingRowViewModel)
}
} // ForEach
} // List
.navigationTitle("Marketplace")
} // NavigationView
}
}
struct MarketplaceView_Previews: PreviewProvider {
static var previews: some View {
MarketplaceView()
}
}
ListingRowView.swift
import SwiftUI
struct ListingRowView: View {
#ObservedObject var listingRowViewModel: ListingRowViewModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(listingRowViewModel.listing.name)
.font(.headline)
Text(String(listingRowViewModel.listing.description))
.font(.footnote)
}
}
}
struct ListingRowView_Previews: PreviewProvider {
static let listingRowViewModel = ListingRowViewModel(listing: testListing1)
static var previews: some View {
ListingRowView(listingRowViewModel: listingRowViewModel)
}
}
ListingDetailView.swift
import SwiftUI
struct ListingDetailView: View {
var listingDetailViewModel: ListingDetailViewModel
var body: some View {
VStack(spacing: 5) {
Text(listingDetailViewModel.listing.name)
.font(.headline)
Text(String(listingDetailViewModel.listing.description))
.font(.footnote)
}
}
}
struct ListingDetailView_Previews: PreviewProvider {
static let listingDetailViewModel = ListingDetailViewModel(listing: testListing1)
static var previews: some View {
ListingDetailView(listingDetailViewModel: listingDetailViewModel)
}
}
MarketplaceViewModel.swift
import Foundation
import SwiftUI
import Combine
class MarketplaceViewModel: ObservableObject {
var listingRepository: ListingRepository
#Published var listingRowViewModels = [ListingRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init(listingRepository: ListingRepository) {
self.listingRepository = listingRepository
self.startCombine()
}
func startCombine() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
ListingRowViewModel.swift
import Foundation
import SwiftUI
import Combine
class ListingRowViewModel: ObservableObject {
var id: String = ""
#Published var listing: Listing
private var cancellables = Set<AnyCancellable>()
init(listing: Listing) {
self.listing = listing
$listing
.receive(on: RunLoop.main)
.compactMap { listing in
listing.id
}
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
}
ListingDetailViewModel.swift
import Foundation
import SwiftUI
import Combine
class ListingDetailViewModel: ObservableObject, Identifiable {
var listing: Listing
init(listing: Listing) {
self.listing = listing
}
}
Listing.swift
import Foundation
import SwiftUI
import FirebaseFirestore
import FirebaseFirestoreSwift
struct Listing: Codable, Identifiable {
#DocumentID var id: String?
var name: String
var description: String
}
ListingRepository.swift
import Foundation
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift
class ListingRepository: ObservableObject {
// MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
// Access to Firestore Database
let db = Firestore.firestore()
#Published var listings = [Listing]()
init() {
startSnapshotListener()
}
// MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
func startSnapshotListener() {
db.collection("listings").addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Listings.")
return
}
self.listings = documents.compactMap { listing in
do {
return try listing.data(as: Listing.self)
} catch {
print(error)
}
return nil
}
}
}
}
}

Related

How can I navigate to a detail view of an item by using an #EnvironmentObject to route the views?

I have the following code in SwiftUI. I am expecting it to navigate from the list view to the PetView() with the proper name showing when tapping on one of the items in the ForEach loop or the button that says "Go to first pet". However, when I tap on an item or the button, the app doesn't do anything. What am I doing wrong? Thank you for your help!
import SwiftUI
#main
struct TestListAppApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(ViewRouter())
}
}
}
import SwiftUI
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ForEach(viewRouter.pets) { pet in
NavigationLink(
destination: PetView(),
tag: pet,
selection: $viewRouter.selectedPet,
label: {
Text(pet.name)
}
)
}
Button("Go to first pet.") {
viewRouter.selectedPet = viewRouter.pets[0]
}
}
}
import Foundation
class ViewRouter: ObservableObject {
#Published var selectedPet: Pet? = nil
#Published var pets: [Pet] = [Pet(name: "Louie"), Pet(name: "Fred"), Pet(name: "Stanley")]
}
import SwiftUI
struct PetView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
Text(viewRouter.selectedPet!.name)
}
}
import Foundation
struct Pet: Identifiable, Hashable {
var name: String
var id: String { name }
}
try this:
#main
struct TestListAppApp: App {
#StateObject var viewRouter = ViewRouter()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(viewRouter)
}
}
}
struct PetView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
if let pet = viewRouter.selectedPet {
Text(pet.name)
} else {
EmptyView()
}
}
}
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
NavigationView {
List {
ForEach(viewRouter.pets) { pet in
NavigationLink(destination: PetView(),
tag: pet,
selection: $viewRouter.selectedPet,
label: {
Text(pet.name)
})
}
Button("Go to first pet.") {
viewRouter.selectedPet = viewRouter.pets[0]
}
}
}
}
}

How to keep a Bool state after Google AdMob RewardAd has finished displaying?

I'm currently developing an application using SwiftUI.
I'm trying to use a google AdMob reward Ad.
I made codes to show reward Ads referring to this article.
I'm trying to show an alert after I finish watching a reward Ad fully using a Bool state from RewardedAdDelegate class but It doesn't work...
How could I solve this problem?
Here are the codes:
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
RewardedAd()
.alert(isPresented: $adDelegate.adFullyWatched){
Alert(title: Text("reward Ad finished"),
message: Text("reward Ad finished"),
dismissButton: .default(Text("OK")))
}
}
}
RewardedAd.swift
import SwiftUI
import GoogleMobileAds
struct RewardedAd: View {
#EnvironmentObject var appState: AppState
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
if adDelegate.adLoaded && !adDelegate.adFullyWatched {
let root = UIApplication.shared.windows.first?.rootViewController
self.adDelegate.rewardedAd!.present(fromRootViewController: root!, delegate: adDelegate)
}
return Text("Load ad").onTapGesture {
self.adDelegate.loadAd()
}
}
}
RewardedAdDelegate
import Foundation
import GoogleMobileAds
class RewardedAdDelegate: NSObject, GADRewardedAdDelegate, ObservableObject {
#Published var adLoaded: Bool = false
#Published var adFullyWatched: Bool = false
var rewardedAd: GADRewardedAd? = nil
func loadAd() {
rewardedAd = GADRewardedAd(adUnitID: "ca-app-pub-3940256099942544/1712485313")
rewardedAd!.load(GADRequest()) { error in
if error != nil {
self.adLoaded = false
} else {
self.adLoaded = true
}
}
}
/// Tells the delegate that the user earned a reward.
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
adFullyWatched = true
print("Reward received with currency: \(reward.type), amount \(reward.amount).")
}
/// Tells the delegate that the rewarded ad was presented.
func rewardedAdDidPresent(_ rewardedAd: GADRewardedAd) {
self.adLoaded = false
print("Rewarded ad presented.")
}
/// Tells the delegate that the rewarded ad was dismissed.
func rewardedAdDidDismiss(_ rewardedAd: GADRewardedAd) {
print("Rewarded ad dismissed.")
}
/// Tells the delegate that the rewarded ad failed to present.
func rewardedAd(_ rewardedAd: GADRewardedAd, didFailToPresentWithError error: Error) {
print("Rewarded ad failed to present.")
}
}
UPDATED
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
VStack{
RewardedAd(adDelegate: self.adDelegate)
.alert(isPresented: $adDelegate.adFullyWatched){
Alert(title: Text(""),
message: Text(""),
dismissButton: .default(Text("OK")))
}
// check adDelegate.adFullyWatched state -> after Ad finish this text shows
if adDelegate.adFullyWatched{
Text("Checked")
}
}
}
}
RewardedAd.swift
import SwiftUI
import GoogleMobileAds
struct RewardedAd: View {
#ObservedObject var adDelegate : RewardedAdDelegate
var body: some View {
if adDelegate.adLoaded && !adDelegate.adFullyWatched {
let root = UIApplication.shared.windows.first?.rootViewController
self.adDelegate.rewardedAd!.present(fromRootViewController: root!, delegate: adDelegate)
}
return Text("Load ad").onTapGesture {
self.adDelegate.loadAd()
}
}
}
ReUPDATED
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
#State var isAlert = false
var body: some View {
VStack{
Button(action: {
self.isAlert = true
}){
Text("alert")
.padding()
}
RewardedAd(adDelegate: self.adDelegate)
.alert(isPresented: self.$isAlert){
Alert(title: Text(""),
message: Text(""),
dismissButton: .default(Text("OK")))
}
// check adDelegate.adFullyWatched state -> after Ad finish this text shows
if adDelegate.adFullyWatched{
Text("Checked")
}
}
.onAppear(){
if adDelegate.adFullyWatched{
self.isAlert = true
}
}
}
}
Xcode: Version 12.0.1
iOS: 13.0
You use different instances of delegate in your views, instead you have to inject delegate from first view into second one.
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
RewardedAd(adDelegate: self.adDelegate) // << here inject !!
// ... other code
and
struct RewardedAd: View {
#EnvironmentObject var appState: AppState
#ObservedObject var adDelegate: RewardedAdDelegate // << here declare !!
// ... other code

How to refresh detail view and master view at the same time in ios14.2 (SwiftUI)

Hi I want to refresh the detail view which is triggered by data change under the master view
My approach is setup a Combine listener and trigger the refresh from the detail view
It was working on ios13 but I can't make it working on ios14.2, any workaround?
Here is my master view.
import SwiftUI
struct MainView: View {
#ObservedObject var viewModel : MainViewModel
init(){
viewModel = MainViewModel.shared
}
var body: some View {
NavigationView{
List {
ForEach(viewModel.games ,id:\.self) { game in
NavigationLink(destination: self.navDest(game: game)){
Text("\(game)")
}
}
}
}
}
func navDest(game: Int) -> some View{
print("games value:",game)
return LazyView(DetailView(game: game ))
}
}
The master view model listen to the event come from detail view and update the value
import Foundation
import SwiftUI
import Combine
class MainViewModel : ObservableObject {
#Published var games : [Int] = []
static let shared = MainViewModel()
private var tickets: [AnyCancellable] = []
init(){
for i in 0...5{
games.append(i)
}
addObservor()
}
func addObservor(){
NotificationCenter.default.publisher(for: .updateGame)
.map{$0.object as! Int}
.sink { [unowned self] (game) in
self.updateGame(game: game)
}.store(in: &tickets)
}
func updateGame(game:Int){
print("updateView index:",game)
self.games[game] = 9999
print("after update",games)
}
}
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel : DetailViewModel
init(game: Int) {
print("init detail",game)
viewModel = DetailViewModel(game:game)
}
var body: some View {
VStack{
Text("\(viewModel.game)")
Button("update"){
viewModel.sendUpdate()
}
}
}
}
When I click the update from detail view, it should refresh the master and detail view at the same time.(The flow is DetailViewModlel->MainViewModlel->Refresh MainView ->Refresh DetailView (which is currently display)) But it not work on iOS 14.2
import Foundation
import SwiftUI
class DetailViewModel : ObservableObject {
var period : String = ""
var game : Int = 0
// init(period:String,game: Int){
// self.period = period
init(game: Int){
self.game = game
}
func sendUpdate(){
NotificationCenter.default.post(name: .updateGame, object: game)
}
}
extension Notification.Name {
static let updateGame = Notification.Name("updateGame")
}
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
DetailView
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel : DetailViewModel
init(game: Int) {
print("init detail",game)
viewModel = DetailViewModel(game:game)
}
var body: some View {
VStack{
Text("\(viewModel.game)")
Button("update"){
viewModel.sendUpdate()
}
}
}
}
I have logged down the debug message, it seem like the data did changed but the view didn't.

Problems with EnvironmentObject in ModalView

I have created a simple List and want to add users to it. My project has CoreDate activated and I have add the following Code to the SceneDelegate:
let userStorage = UserStorage()
let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(userStorage)
The code of the ContentView is this:
import SwiftUI
struct User: Identifiable {
var id = UUID()
var firstName = ""
var lastName = ""
}
class UserStorage: ObservableObject {
#Published var users = [User]()
}
struct ContentView: View {
#State private var presentation = false
#EnvironmentObject var userStorage: UserStorage
var body: some View {
VStack {
Button(action: {
self.presentation = true
}) {
Text("New User")
}.sheet(isPresented: $presentation, onDismiss: {
self.presentation = false
}) {
newuserView(presentation: self.$presentation, newUser: User())
}
List(userStorage.users) { singleUser in
VStack {
Text(singleUser.firstName)
Text(singleUser.lastName)
}
}
}
}
}
struct newuserView : View {
#Binding var presentation: Bool
#State var newUser: User
#EnvironmentObject var userStarage: UserStorage
var body: some View {
VStack {
TextField("Put in first name please", text:$newUser.firstName)
TextField("Put in last name please", text:$newUser.lastName)
Button(action: {
self.userStarage.users.append(self.newUser)
self.presentation = false
}) {
Text("Add new User")
}disabled(newUser.lastName.isEmpty || newUser.firstName.isEmpty)
}.padding(.horizontal)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When I run my project and want to add a user, I get the following error:
Thread 1: Fatal error: No ObservableObject of type UserStorage found.
A View.environmentObject(_:) for UserStorage may be missing as an ancestor of this view.
I have tried to do this with .sheet, but it doesn't work
Sheet creates different view hierarchy so .environmentObject is not injected in view to be shown in sheet by default - you have to do it manually
}.sheet(isPresented: $presentation, onDismiss: {
self.presentation = false
}) {
newuserView(presentation: self.$presentation, newUser: User())
.environmentObject(self.userStorage)
}

Invalidate List SwiftUI

Workaround at bottom of Question
I thought SwiftUI was supposed to automatically update views when data they were dependent on changed. However that isn't happening in the code below:
First I make a simple BindableObject
import SwiftUI
import Combine
class Example: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var test = 1 {
didSet {
didChange.send(())
}
}
}
Then the root view of the app:
struct BindTest : View {
#Binding var test: Example
var body: some View {
PresentationButton(destination: BindChange(test: $test)) {
ForEach(0..<test.test) { index in
Text("Invalidate Me! \(index)")
}
}
}
}
And finally the view in which I change the value of the BindableObject:
struct BindChange : View {
#Binding var test: Example
#Environment(\.isPresented) var isPresented
var body: some View {
Button(action: act) {
Text("Return")
}
}
func act() {
test.test = 2
isPresented?.value = false
}
}
When the return button is tapped there should be 2 instances of the Text View - but there is only 1. What am I doing wrong?
Also worth noting: If I change the #Binding to #EnvironmentObject the program just crashes when you tap the button producing this error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Full code below:
import SwiftUI
import Combine
class Example: BindableObject {
var didChange = PassthroughSubject<Example, Never>()
var test = 1 {
didSet {
didChange.send(self)
}
}
static let `default` = {
return Example()
}()
}
//Root View
struct BindTest : View {
#EnvironmentObject var test: Example
var body: some View {
PresentationButton(destination: BindChange()) {
ForEach(0..<test.test) { t in
Text("Invalidate Me! \(t)")
}
}
}
}
//View that changes the value of #Binding / #EnvironmentObject
struct BindChange : View {
#EnvironmentObject var test: Example
#Environment(\.isPresented) var isPresented
var body: some View {
Button(action: act) {
Text("Return")
}
}
func act() {
test.test = 2
isPresented?.value = false
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
//ContentView().environmentObject(EntryStore())
BindTest().environmentObject(Example())
}
}
#endif
EDIT 2: Post's getting a little messy at this point but the crash with EnvironmentObject seems to be related to an issue with PresentationButton
By putting a NavigationButton inside a NavigationView the following code produces the correct result - invalidating the List when test.test changes:
//Root View
struct BindTest : View {
#EnvironmentObject var test: Example
var body: some View {
NavigationView {
NavigationButton(destination: BindChange()) {
ForEach(0..<test.test) { t in
Text("Functional Button #\(t)")
}
}
}
}
}