SwiftUI中如何先初始化单例再初始化ViewModel
解决方案
1. 修复UserManager单例实现
首先确保UserManager是真正的全局单例,避免多实例导致的数据不一致,同时调整用户数据加载逻辑,便于外部监听状态:
class UserManager: ObservableObject { // 全局唯一单例 static let shared = UserManager() // 私有初始化,禁止外部创建实例 private init() {} let db = Firestore.firestore() @Published var currentUser: UserDocument? // 可选:添加加载状态,用于视图显示加载指示器 @Published var isLoadingUser = false let firebaseManager = FirestoreService() // 显式触发用户数据加载 func fetchCurrentUser() { let email = getCurrentUserEmail() guard !email.isEmpty else { return } isLoadingUser = true getDocument(user: email) { [weak self] doc in self?.currentUser = doc self?.isLoadingUser = false } } private func getCurrentUserEmail() -> String { Auth.auth().currentUser?.email ?? "" } func getDocument(user: String, completion: @escaping (UserDocument?) -> Void) { guard !user.isEmpty else { completion(nil) return } let doc = db.collection("Users").document(user) doc.getDocument { documentSnapshot, error in if let error = error { print("Error fetching document: \(error)") completion(nil) return } guard let data = documentSnapshot?.data(), let firstName = data["FirstName"] as? String, let lastName = data["LastName"] as? String, let college = data["College"] as? String, let birthday = data["Birthday"] as? String, let major = data["Major"] as? String, let classes = data["Classes"] as? [String], let email = data["Email"] as? String, let profilePictureURL = data["profile_picture"] as? String else { print("Invalid document data or missing fields") completion(nil) return } let retrievedDoc = UserDocument(FirstName: firstName, LastName: lastName, College: college, Birthday: birthday, Major: major, Classes: classes, Email: email, profilePictureURL: profilePictureURL) completion(retrievedDoc) } } }
2. 调整ViewModel,监听UserManager状态变化
移除ViewModel初始化时直接调用依赖用户数据的方法,改为监听UserManager的currentUser变化,当用户数据准备完成后自动触发数据加载:
ClassPostsViewModel 修改:
import Combine class ClassPostsViewModel: ObservableObject { @Published var postsArray: [ClassPost] = [] @Published var repliesArray: [Reply] = [] @Published var curError: String = "" @Published var isLoading: Bool = false @Published var selectedClass: String = "" let firebaseManager = FirestoreService() let db = Firestore.firestore() // 用于管理订阅生命周期 private var cancellables = Set<AnyCancellable>() init() { // 监听UserManager的currentUser变化,仅在有数据时执行加载 UserManager.shared.$currentUser .compactMap { $0 } // 过滤掉nil值 .sink { [weak self] _ in self?.getPosts() } .store(in: &cancellables) } func getPosts() { guard !self.selectedClass.isEmpty, let college = UserManager.shared.currentUser?.College else { print("Error: Class or college is undefined") return } isLoading = true firebaseManager.fetchPosts(fromClass: self.selectedClass, fromCollege: college) { [weak self] (posts, error) in self?.isLoading = false if let error = error { self?.curError = "Something went wrong getting the posts for \(self?.selectedClass ?? "this class")" return } self?.postsArray = posts ?? [] } } }
UserProfileViewModel 修改:
import Combine class UserProfileViewModel: ObservableObject { @Published var usersPosts: [ClassPost] = [] @Published var curError: String = "" let db = Firestore.firestore() let firebaseManager = FirestoreService() private var cancellables = Set<AnyCancellable>() init() { UserManager.shared.$currentUser .compactMap { $0 } .sink { [weak self] _ in self?.getPostsForUser() } .store(in: &cancellables) } func getPostsForUser() { guard let college = UserManager.shared.currentUser?.College else { return } firebaseManager.getPostsForUser(college: college) { [weak self] posts, error in guard let self = self else { return } if let error = error { self.curError = "Something went wrong getting your posts. Try Again." } else { self.usersPosts = posts ?? [] } } } }
3. 调整TabView,触发用户数据加载
在TabView的onAppear中触发UserManager的用户数据加载,ViewModel会自动响应数据完成事件:
struct InAppView: View { @StateObject var viewRouter: ViewRouter @StateObject var userProfile = UserProfileViewModel() @StateObject var posts = ClassPostsViewModel() let customAccentColor = Color(red: 75 / 255, green: 175 / 255, blue: 210 / 255) var body: some View { // 可选:添加加载状态提示 Group { if UserManager.shared.isLoadingUser { ProgressView("Loading user data...") } else { TabView() { ClassPosts(viewRouter: viewRouter) .environmentObject(userProfile) .environmentObject(posts) .tabItem { VStack { Image(systemName: "list.bullet") Text("Posts") } } .tag(1) UserProfileView(viewRouter: viewRouter) .environmentObject(userProfile) .environmentObject(posts) .tabItem { VStack { Image(systemName: "person.fill") Text("Profile") } } .tag(0) } } } .accentColor(customAccentColor) .onAppear { // 触发用户数据加载 UserManager.shared.fetchCurrentUser() // TabBar样式设置 let uiColor = UIColor(red: 75 / 255, green: 175 / 255, blue: 210 / 255, alpha: 1.0) UITabBar.appearance().isTranslucent = true UITabBar.appearance().backgroundImage = UIImage() UITabBar.appearance().shadowImage = UIImage() UITabBar.appearance().unselectedItemTintColor = UIColor.systemGray3 UITabBar.appearance().tintColor = uiColor } } }
方案说明
- 单例可靠性:
UserManager通过static let shared和私有初始化保证全局唯一实例,彻底避免多实例数据不一致问题。 - 响应式触发:ViewModel通过Combine监听
UserManager的currentUser发布者,当用户数据加载完成后自动执行帖子加载逻辑,无需等待ViewModel初始化时机,完全符合SwiftUI的响应式编程范式。 - 加载状态优化:新增
isLoadingUser状态,可在视图中显示加载指示器,提升用户体验。
关于你提到的“在TabView的onAppear中通过回调初始化UserManager再初始化ViewModel”方案:该方案可行,但需要将ViewModel改为可选类型(如@StateObject var userProfile: UserProfileViewModel?),在回调中赋值。但这种方式不如响应式监听灵活,且会增加可选类型的处理成本,推荐使用上述响应式方案。
内容的提问来源于stack exchange,提问作者Kyle
相关产品推荐
相关产品推荐

