SwiftUI环境对象访问异常问题:登录后解码数据无法在子视图中读取
Hey there! I see exactly what's going on here—you're dealing with a classic instance mismatch issue in SwiftUI. Let's break it down and get your data showing up properly.
The Core Problem
You've got two separate instances of AuthViewModel:
- One created in
ContentViewas@StateObject var test: AuthViewModel = AuthViewModel()(this is the one yourSearchHistoryview tries to access via@EnvironmentObject) - Another created in
SignInview as@StateObject var vm: AuthViewModel = AuthViewModel()(this is the one that actually gets the decodedclaimHistroydata after login)
Since these are completely separate objects, the data stored in the SignIn view's vm never makes it to the ContentView's test instance. That's why vm.claimHistroy is always nil in SearchHistory!
Step-by-Step Fix
1. Share a Single AuthViewModel Instance Across Your App
Instead of creating a new AuthViewModel in SignIn, use the one from the environment. Here's how to adjust your code:
Update the SignIn View
Remove the local @StateObject and use @EnvironmentObject instead:
struct SignIn: View { @EnvironmentObject var vm: AuthViewModel // Now uses the shared instance @EnvironmentObject var authentication: AuthenticationCheck @Binding var userID: String @Binding var passcode: String var body: some View { Button(action: { print("Button action") vm.signIn(username: userID, password: passcode) }) { Text("Sign In") .multilineTextAlignment(.center) .padding() } .frame(width: 150.0, height: 43.0) } }
Inject the ViewModel at the App Root
For best practice, create your AuthViewModel at the app's entry point and inject it into the environment there, so every view can access it:
@main struct YourAppName: App { @StateObject private var authVM = AuthViewModel() @StateObject private var authCheck = AuthenticationCheck() var body: some Scene { WindowGroup { // Toggle between SignIn and ContentView based on auth status if authCheck.isValidated { ContentView() } else { SignIn(userID: .constant(""), passcode: .constant("")) } } .environmentObject(authVM) .environmentObject(authCheck) } }
2. Clean Up ContentView
You can remove the local @StateObject in ContentView since we're now injecting from the root:
struct ContentView: View { var body: some View { TabView { Home() .tabItem { Image(systemName: "house.fill") Text("Home") } Search() .font(.system(size: 30, weight: .bold, design: .rounded)) .tabItem { Image(systemName: "magnifyingglass") Text("Search") } Text("Profile Page") .font(.system(size: 30, weight: .bold, design: .rounded)) .tabItem { Image(systemName: "person.fill") Text("Profile") } } } }
Bonus Code Optimizations
Since you're new to Swift, here are some tweaks to make your code more robust:
1. Simplify NetworkService's handleResponse
You're converting data to a JSON object and then decoding it—this is redundant. Clean it up:
private func handleResponse<T: Decodable>(result: Result<Data, Error>?, completion: (Result<T, Error>) -> Void) { guard let result = result else { completion(.failure(AppError.unknownError)) return } switch result { case .success(let data): let decoder = JSONDecoder() do { let decodedData = try decoder.decode(T.self, from: data) print("Decoded response: \(decodedData)") completion(.success(decodedData)) } catch { print("Decoding error: \(error)") completion(.failure(AppError.errorDecoding)) } case .failure(let error): completion(.failure(error)) } }
2. Improve Optional Handling in SearchHistory
Instead of String(describing:), use a user-friendly default value:
Text(vm.claimHistroy?.result.member.yearToDateSearchSavingsAmount ?? "0.00")
Or conditionally show content:
if let savings = vm.claimHistroy?.result.member.yearToDateSearchSavingsAmount { Text(savings) } else { Text("No data available") }
3. Encapsulate AuthViewModel State
Add private(set) to your @Published properties to prevent external views from modifying state directly:
class AuthViewModel: ObservableObject { @Published private(set) var user: LoginResponseData.Root? = nil @Published private(set) var alert: CustomAlert? = nil @Published private(set) var claimHistroy: ClaimStruct? = nil // ... rest of your code }
4. Merge AuthenticationCheck into AuthViewModel
You don't need a separate object—let AuthViewModel handle validation state:
class AuthViewModel: ObservableObject { @Published private(set) var isValidated = false // ... rest of your properties func signIn(username: String, password: String) { networkService.signIn(username: username, password: password) { (result) in switch result { case .success(let user): self.user = user self.isValidated = true self.profileSessionMember() case .failure(let error): print("Error: \(error.localizedDescription)") self.isValidated = false self.alert = CustomAlert(title: "Invalid Credentials", message: "Either username or password is incorrect. Please try again") } } } func logout() { self.user = nil self.isValidated = false } // ... rest of your code }
Then you can remove the AuthenticationCheck class entirely and use authVM.isValidated in your root flow.
内容的提问来源于stack exchange,提问作者Kwabena Darkwa

