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

SwiftUI从Firestore获取用户数据时短暂闪现默认值问题

问题根源

出现"Name Unavailable"闪现的核心原因有三个:

  1. 加载兜底和错误兜底逻辑混淆:页面首次渲染时userModel默认值为nil,空合并运算符直接触发了错误兜底文案展示,但此时Firestore的异步请求还在网络传输过程中,根本不是真的用户名获取失败。
  2. View里用@ObservedObject声明ViewModel会导致每次视图重绘都重新初始化DashboardLogic,重复触发Firestore请求,平白拉长加载等待时间。
  3. 没有利用Firestore的本地缓存能力,每次进页面都纯等网络返回,冷启动场景下必然有空白等待窗口。

修复方案

1. 修正ViewModel的声明方式

把View里的@ObservedObject换成@StateObject,这是SwiftUI专门给View持有的Observable对象设计的属性包装器,只会在View首次加载时初始化一次ViewModel,不会重复触发请求。

2. 拆分三类UI状态

不要用错误提示当加载占位,专门加状态标记区分不同场景:

  • 加载中:展示占位骨架/加载提示,绝对不显示错误文案
  • 加载失败:才展示"Name Unavailable"
  • 加载成功:展示真实用户名

3. 优先读取Firestore本地缓存

Firestore默认会缓存已拉取过的数据,优先读缓存可以做到二次进入页面秒出内容,缓存读完后静默拉取服务端最新数据更新即可,用户完全感知不到加载过程。

4. 补全内存泄漏防护

Firestore的异步回调里要加[weak self],避免循环引用导致的内存泄漏。


修改后代码

DashboardLogic 部分

注意:原代码里privateUserModel结构体首字母小写不符合Swift命名规范,统一调整为大驼峰命名PrivateUserModel

class DashboardLogic: ObservableObject {
    @Published var userModel: UserModel?
    @Published var privateUserModel: PrivateUserModel?
    // 标记数据是否在加载中
    @Published var isLoadingUserData = true
    // 标记数据是否加载失败
    @Published var loadUserFailed = false
    
    init(){
        fetchCurrentUser()
    }

    private func fetchCurrentUser () {
        // 拉取前重置状态
        isLoadingUserData = true
        loadUserFailed = false
        
        guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
            isLoadingUserData = false
            loadUserFailed = true
            return
        }
        guard FirebaseManager.shared.auth.currentUser?.email != nil else {
            print("could not locate email")
            isLoadingUserData = false
            loadUserFailed = true
            return
        }
        
        // 先拉取本地缓存,无网络也能立刻返回数据
        FirebaseManager.shared.firestore
            .collection("users").document(uid)
            .getDocument(source: .cache) { [weak self] snapshot, _ in
                guard let self = self else { return }
                if let data = snapshot?.data() {
                    self.userModel = .init(data: data)
                }
                
                // 缓存拉完静默拉取服务端最新数据
                FirebaseManager.shared.firestore
                    .collection("users").document(uid)
                    .getDocument(source: .server) { [weak self] snapshot, error in
                        guard let self = self else { return }
                        // 无论成功失败,网络请求结束就关闭loading
                        defer { self.isLoadingUserData = false }
                        
                        if let error = error {
                            print("failed to fetch user \(error)")
                            // 缓存也没拿到数据才标记失败
                            self.loadUserFailed = self.userModel == nil
                            return
                        }
                    
                        guard let data = snapshot?.data() else {
                            print("no data found for user")
                            self.loadUserFailed = self.userModel == nil
                            return
                        }
                        self.userModel = .init(data: data)
                    }
            }

        // 私有用户信息同理,优先读缓存
        FirebaseManager.shared.firestore
            .collection("users").document(uid)
            .collection("privateUserInfo")
            .document("private")
            .getDocument(source: .cache) { [weak self] snapshot, _ in
                guard let self = self else { return }
                if let data = snapshot?.data() {
                    self.privateUserModel = .init(data: data)
                }
                
                FirebaseManager.shared.firestore
                    .collection("users").document(uid)
                    .collection("privateUserInfo")
                    .document("private")
                    .getDocument(source: .server) { [weak self] snapshot, error in
                        guard let self = self else { return }
                        if let error = error {
                            print("fetch private user info failed \(error)")
                            return
                        }
                        guard let userEmail = snapshot?.data() else {
                            print("no email found for user")
                            return
                        }
                        self.privateUserModel = .init(data:userEmail)
                    }
            }
    }
}

UserDashController 部分

struct UserDashController: View {
    // 替换为@StateObject,避免重复初始化ViewModel
    @StateObject var vm = DashboardLogic()
  
    @State private var action: Int? = 0
    @State private var userSigningOut = false
    @State private var showMenu = false
    @State private var presentSettingsPage = false
   
    var body: some View {
        NavigationView{
            VStack{
                HStack{
                    // 根据状态展示不同UI
                    if vm.isLoadingUserData {
                        // 加载中用redacted做占位骨架,和真实文本布局一致,不会跳动
                        Text("Placeholder")
                            .redacted(reason: .placeholder)
                    } else if vm.loadUserFailed {
                        // 真的失败才展示错误文案
                        Text("Name Unavailable")
                    } else {
                        Text(vm.userModel?.name ?? "")
                    }
                }
                .padding()
            }
            .padding(.top, -5)
        } 
    }
}

UserModel 部分

仅调整私有模型的命名规范即可:

struct UserModel {
    var uid, name, gender, height, weight, agenda, profilePictureURL: String
    
    init(data: [String: Any]){
        self.uid = data["uid"] as? String ?? "Unavailable"
        self.name = data["name"] as? String ?? "Unavailable"
        self.gender = data["gender"] as? String ?? "Unavailable"
        self.height = data["height"] as? String ?? "Unavailable"
        self.weight = data["weight"] as? String ?? "Unavailable"
        self.agenda = data["agenda"] as? String ?? "Unavailable"
        self.profilePictureURL = data ["profilePicture"] as? String ?? "Unavailable"
    }
}

// 结构体名统一用大驼峰
struct PrivateUserModel {
    var email: String
    
    init(data: [String: Any]){
        self.email = data["email"] as? String ?? "Unavailable"
    }
}

额外优化建议

  • 如果对实时性要求高,可以把getDocument换成addSnapshotListener,第一次返回会自动优先推送缓存数据,后续服务端数据变化会自动同步,不用手动发两次请求。
  • 可以在用户登录成功后就提前预拉取用户数据存到全局,进入个人资料页时直接读预加载的数据,完全消除加载等待。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 00:31:16