You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 06:47:01