I'm facing an issue with swiftUI where my view doesn't update when an isSignedIn property changes in a sessionManager.
Setup Overview:
I have an 'AppState' object which encapsulates several managers, including a 'SessionManager'. The 'SessionManager' handles user auth and has a published property 'isSignedIn' to track user sign in status. Despite correctly updating 'isSignedIn' after a successful sign-in, my 'MainView' does not reflect these changes until I manually trigger a state change (pressing a test button). Even after the changes are manually reflected, the 'MainView' does not update as expected.
Code Snippets:
// SessionManager
import Foundation
import SwiftUI
class SessionManager: ObservableObject {
@Published private(set) var session: Session? = nil
@Published private(set) var user: User? = nil
@Published private(set) var isSignedIn: Bool = false
// These signIn and signOut functions are working as expected
func signIn(email: String, password: String) async throws {
try await performSignIn(email: email, password: password)
}
func signOut() {
Task {
await performSignOut()
}
}
// AppState
class AppState: ObservableObject {
@Published var sessionManager: SessionManager
@Published var eventManager: EventManager
@Published var locationManager: LocationManager
init() {
let session = SessionManager()
self._sessionManager = Published(wrappedValue: session)
self._eventManager = Published(wrappedValue: EventManager(sessionManager: session)).
self._locationManager = Published(wrappedValue: LocationManager(sessionManager: session)).
}
}
// App
import SwiftUI
@main
struct myApp: App {
@StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(appState)
}
}
}
}
// MainView
import SwiftUI
@_spi(Experimental) import MapboxMaps
struct MainView: View {
@EnvironmentObject var appState: AppState
@State var selectedTab = "MapView"
var body: some View {
Group {
// display based on sign in status
if appState.sessionManager.isSignedIn {
NormalView()
// if not signed in, display login form
} else {
ZStack {
LoginView(LoginViewModel(appState))
VStack {
Spacer()
// after a successful sign in, pressing this button prints true to the console as expected
Button("test") {
print("Test!!! \(appState.sessionManager.isSignedIn)")
}
// but after a successful sign in, this text doesn't update, and neither does the main view...
Text("\(appState.sessionManager.isSignedIn ? "Signed In" : "Not Signed In")")
}
}
}
}
}
}
Debugging Steps Taken
Print Statements: Confirmed that isSignedIn changes to true upon successful sign-in. Environment Object: Verified that AppState is correctly passed as an environment object to MainView. Memory Addresses: Confirmed that the same instances of AppState and its properties are being used throughout the app.
Request for help
I'd greatly appreciate any insights or suggestions on what might be causing the view not to update automatically when 'isSignedIn' changes, and how to resolve this issue. If more information is needed, please let me know.
Thank you!
This seems like a classic nested ObservableObjects issue in SwiftUI. Without going into much details, you could look up "The Nested Observables Problem in SwiftUI" for various variations of the issue.
Basically, when a parent object holds a child object that is also an ObservableObject SwiftUI does not automatically update the view to reflect changes in the child object.
I would suggest you either move to Observable macro, Unlike ObservableObject nested Observables just work.
Another alternative is to
- Explicitly Observe the childObjects (SessionManager, LocationManager, EventManager) in the view.
Or use Combine to makes sure that when the child changes, the ParentObject informs SwiftUI that an update is necessary.
For example:
class ParentObject: ObservableObject {
@Published var child: ChildObject = ChildObject()
private var cancellable: AnyCancellable?
init() {
cancellable = $child.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
}