SwiftUI Combine EnvironmentObject ObservableObject

Hi CodeCrew!

I am trying to rewrite my app using SwiftUI only and I am having difficulty with the EnvironmentObject, trying to understand how it works…

I want to redirect my app users to the appropriate page at launch, depending on:

  • if this is their first time
  • if they have a login,
  • if they want to start using without login

If it is the first time the app is launched, LocalStorage has no data so I present the app on a welcome page
I offer the choice of 2 buttons to click on:

  • “New User” which redirect to the main page of the app and create a new user
  • “Login” which present the login page to retrieve the last backup

If the app has previously been launched, I present the main page straight away.

Now said, if I initiate my “currentPage” as “MainView” or “LoginView”, it works - but NOT if it is set as “WelcomeView”.
I presume the problem comes when the variable gets changed from a subview? I thought the use of @EnvironmentObject was the way to get around this…

Can someone explain to me how it works?

My various files are:

import SwiftUI
import Combine

class ViewRouter: ObservableObject {
    
    let objectWillChange = PassthroughSubject<ViewRouter,Never>()
    
    var currentPage: String = "WelcomeView" {
        didSet {
            objectWillChange.send(self)
        }
    }
}
import SwiftUI

struct ParentView : View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            if viewRouter.currentPage == "WelcomeView" {
                WelcomeView()
            }
            else if viewRouter.currentPage == "MainView" {
                MainView()
            }
            else if viewRouter.currentPage == "LoginView" {
                LoginView()
            }
        }
    }
}
import SwiftUI

struct WelcomeView: View {
    
    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        ZStack{
            // VStack { [some irrelevant extra code here] }
            VStack {
                    LoginButtons().environmentObject(ViewRouter())
            }
            // VStack { [some irrelevant extra code here] }
        }
    }
}
import SwiftUI

struct LoginButtons: View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {

            Button(action: {
                self.viewRouter.currentPage = "MainView"
            }) {
                Text("NEW USER")
            }
            
            Button(action: {
                self.viewRouter.currentPage = "LoginView"
            }) {
                Text("I ALREADY HAVE AN ACCOUNT")
            }
        }
    }
}
import SwiftUI

struct MainView : View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            // Just want to check if it is working for now before implementing the appropriate Views...
            Button(action: {
                self.viewRouter.currentPage = "WelcomeView"
            }) {
                Text("BACK")
            }
        }
    }
}
import SwiftUI

struct LoginView : View {
    
    @EnvironmentObject var viewRouter: ViewRouter
    
    var body: some View {
        VStack {
            // Just want to check if it is working for now before implementing the appropriate Views...
            Button(action: {
                self.viewRouter.currentPage = "WelcomeView"
            }) {
                Text("BACK")
            }
        }
    }
}

Many Thanks in advance! :wink:

Alright…

After few hours of research and tests, I finally found my ‘mistake’!
It was to add an extra “.environmentObject(ViewRouter())” when calling my subview “LoginButtons”.

If I remove it, it works!

But why??
Could someone explain?
I still very confused…

struct WelcomeView: View {

    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        ZStack{
            // VStack { [some irrelevant extra code here] }
            VStack {
                    LoginButtons()
                       // -->     .environmentObject(ViewRouter())
            }
            // VStack { [some irrelevant extra code here] }
        }
    }
}

Hi Cedric

Have you tried using @StateObeject in in the app.swift file? It’s the one that creates the ContentView (root view).

If you create an instance of ViewRouter in this file and use the @StateObject property wrapper, that will enable all child views to access that by declaring your ViewRouter using @EnvironmentObject wrapper.

This line creates a new ViewRouter object and sticks it in the environment rather than using the existing ViewRouter that is already in the environment.

1 Like