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

Coordinator模式下:等待异步加载用户信息后设置window.rootViewController

解决Firebase用户信息加载完成前设置RootViewController的问题

核心问题就是你现在是同步返回ViewController,但Firebase拉取用户信息是异步操作,导致TabCoordinator启动时currentUser还没赋值,直接触发了fatalError。下面给你一套可行的改造方案:

第一步:先返回过渡加载页

因为你的Coordinator协议要求start必须返回UIViewController,所以先给用户展示一个加载中的页面,等用户信息拉完再替换成真正的TabVC。

先写个简单的加载页:

class LoadingViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        let spinner = UIActivityIndicatorView(style: .large)
        spinner.translatesAutoresizingMaskIntoConstraints = false
        spinner.startAnimating()
        view.addSubview(spinner)
        
        NSLayoutConstraint.activate([
            spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

第二步:改造MainCoordinator的showTabVC方法

在调用fetchCurrentUser后,订阅Combine的Publisher,等拿到用户信息后再初始化TabCoordinator并替换根控制器:

首先在MainCoordinator里加个存储订阅的属性:

private var cancellables = Set<AnyCancellable>()

然后修改showTabVC:

func showTabVC() -> UIViewController {
    guard let user = Auth.auth().currentUser else { fatalError() }
    dataManager = DataManager(currentUserUid: user.uid)
    
    // 先返回加载页
    let loadingVC = LoadingViewController()
    
    // 订阅用户信息加载完成事件
    dataManager?.fetchCurrentUser(user: user.uid)
        .receive(on: DispatchQueue.main) // 切回主线程更新UI
        .sink(receiveCompletion: { completion in
            // 处理加载失败的情况,比如弹窗提示
            if case .failure(let error) = completion {
                print("用户信息加载失败:\(error.localizedDescription)")
            }
        }, receiveValue: { [weak self] _ in
            guard let self = self else { return }
            // 用户信息加载完成,初始化TabCoordinator
            self.tabCoordinator = TabCoordinator(tabBarController: self.rootViewController as! UITabBarController, dataManager: self.dataManager!)
            let tabVC = self.tabCoordinator.start()
            
            // 替换根控制器,加个过渡动画更友好
            if let window = UIApplication.shared.windows.first {
                UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve) {
                    window.rootViewController = tabVC
                }
            }
        })
        .store(in: &cancellables) // 保存订阅,防止被提前释放
    
    return loadingVC
}

第三步:修复DataManager的错误处理

你原来的代码里有强制解包,也没处理错误,容易崩,改一下:

func fetchUser(uid: String, completion: @escaping (UserInfo?, Error?) -> Void) {
    DataManager.userInfoRef.child(uid).observeSingleEvent(of: .value) { snapshot, error in
        if let error = error {
            completion(nil, error)
            return
        }
        
        // 不要强制解包,先判断解析结果
        guard let user = UserInfo(snapshot: snapshot as DataSnapshot) else {
            let parseError = NSError(domain: "DataManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "用户信息解析失败"])
            completion(nil, parseError)
            return
        }
        
        self.currentUser = user
        completion(user, nil)
    }
}

// 把原来的Never改成Error,能处理加载失败的情况
func fetchCurrentUser(user uid: String) -> AnyPublisher<UserInfo, Error> {
    return Future<UserInfo, Error> { promise in
        self.fetchUser(uid: uid) { result, error in
            if let error = error {
                promise(.failure(error))
            } else if let result = result {
                promise(.success(result))
            } else {
                let noDataError = NSError(domain: "DataManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "无用户数据"])
                promise(.failure(noDataError))
            }
        }
    }
    .handleEvents(receiveOutput: { [weak self] value in
        self?.currentUser = value
    })
    .eraseToAnyPublisher()
}

第四步:调整TabCoordinator的start方法

别再用fatalError了,给个兜底方案:

func start() -> UIViewController {
    guard let currentUser = dataManager.currentUser else {
        // 加载失败的情况,返回提示页或者默认用户流程
        let errorVC = UIViewController()
        errorVC.view.backgroundColor = .white
        let label = UILabel()
        label.text = "用户信息加载失败,请重试"
        label.translatesAutoresizingMaskIntoConstraints = false
        errorVC.view.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: errorVC.view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: errorVC.view.centerYAnchor)
        ])
        return errorVC
        // 或者直接返回用户流程:return userFlow()
    }
    
    return currentUser.admin ? adminFlow() : userFlow()
}

最后:SceneDelegate不用改

原来的代码保持不变就行,因为现在showTabVC返回的是加载页,后续会自动替换成正确的TabVC。

内容的提问来源于stack exchange,提问作者Dzondzula

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 23:07:48