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
相关产品推荐
相关产品推荐

