SwiftUI从Firestore获取用户数据时短暂闪现默认值问题
问题根源
出现"Name Unavailable"闪现的核心原因有三个:
- 加载兜底和错误兜底逻辑混淆:页面首次渲染时
userModel默认值为nil,空合并运算符直接触发了错误兜底文案展示,但此时Firestore的异步请求还在网络传输过程中,根本不是真的用户名获取失败。 - View里用
@ObservedObject声明ViewModel会导致每次视图重绘都重新初始化DashboardLogic,重复触发Firestore请求,平白拉长加载等待时间。 - 没有利用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
相关产品推荐
相关产品推荐

