SwiftUI+Firebase:视图先加载致ViewModel用户数据未同步问题
Swift Firebase异步加载问题:Poll用户信息不显示及重复请求解决
问题描述
跟随教程开发时,FeedViewModel调用PollService获取poll列表后,循环通过UserService从Firebase获取关联用户,但因Firebase异步特性,「Fetch all polls」日志先于「Fetched user」执行,导致poll的user属性未及时赋值,视图中无法显示用户信息;且日志显示仅1条poll时循环执行两次,尝试DispatchGroup未解决。
问题原因
- 异步执行顺序问题:Firestore的
getDocument是异步操作,循环中发起用户请求后会直接继续执行后续代码,因此「Fetch all polls」会在用户信息获取完成前打印,此时poll的user属性尚未赋值,视图无法加载用户信息。同时直接修改数组内元素不会触发@Published的更新通知,因为数组是值类型,修改内部元素不会触发属性包装器的监听。 - ViewModel重复初始化:日志出现两次循环和「Fetch all polls」,大概率是父视图中创建FeedViewModel时使用了
@State而非@StateObject,导致ViewModel被多次实例化,fetchPolls方法重复执行。
优化方案
1. 正确使用DispatchGroup处理异步请求
修正DispatchGroup的用法,确保所有用户请求完成后再更新polls数组,同时通过重新赋值数组触发@Published更新:
class FeedViewModel: ObservableObject { @Published var polls = [Poll]() let service = PollService() let userService = UserService() init() { fetchPolls() } func fetchPolls() { service.fetchPolls { [weak self] polls in guard let self = self else { return } let group = DispatchGroup() var updatedPolls = polls for (index, poll) in updatedPolls.enumerated() { group.enter() self.userService.fetchUser(withUid: poll.userId) { user in updatedPolls[index].user = user print("DEBUG: Fetched user") group.leave() } print("DEBUG: Fetched one poll") } group.notify(queue: .main) { self.polls = updatedPolls print("DEBUG: Fetch all polls") } } } }
2. 避免ViewModel重复初始化
在父视图中使用@StateObject创建FeedViewModel,确保其在视图生命周期内保持唯一实例:
struct FeedView: View { @StateObject var viewModel = FeedViewModel() // 替代@State var body: some View { ScrollView { ForEach(viewModel.polls) { poll in PollView(poll: poll) } } } }
3. 改用Async/Await简化异步代码(推荐)
若项目支持iOS 15+/macOS 12+,用Async/Await替代completion handler,代码更简洁易读:
先修改服务层方法为异步:
// UserService func fetchUser(withUid uid: String) async throws -> User { let snapshot = try await Firestore.firestore().collection("users").document(uid).getDocument() guard let user = try? snapshot.data(as: User.self) else { throw NSError(domain: "UserError", code: 0, userInfo: [NSLocalizedDescriptionKey: "解析用户失败"]) } return user } // PollService func fetchPolls() async throws -> [Poll] { let snapshot = try await Firestore.firestore().collection("polls").getDocuments() return snapshot.documents.compactMap { try? $0.data(as: Poll.self) } }
再更新FeedViewModel:
class FeedViewModel: ObservableObject { @Published var polls = [Poll]() let service = PollService() let userService = UserService() init() { Task { await fetchPolls() } } func fetchPolls() async { do { let polls = try await service.fetchPolls() var updatedPolls = polls for (index, poll) in updatedPolls.enumerated() { do { let user = try await userService.fetchUser(withUid: poll.userId) updatedPolls[index].user = user print("DEBUG: Fetched user") } catch { print("DEBUG: 获取用户失败: \(error.localizedDescription)") } print("DEBUG: Fetched one poll") } await MainActor.run { self.polls = updatedPolls print("DEBUG: Fetch all polls") } } catch { print("DEBUG: 获取Poll列表失败: \(error.localizedDescription)") } } }
4. 视图优化用户信息显示
确保Poll遵循Identifiable协议,同时添加占位符优化加载状态:
struct Poll: Identifiable { let id: String let userId: String var user: User? // 其他属性 } struct PollView: View { let poll: Poll // 用let而非@State,数据由ViewModel驱动 var body: some View { VStack { HStack { Circle().frame(width: 40, height: 40) HStack { Text(poll.user?.username ?? "加载中...") .padding(.leading) Spacer() Text("Date") .font(.footnote) .fontWeight(.light) } } // Poll其他内容 } } }
内容的提问来源于stack exchange,提问作者DJ_JONES
相关产品推荐
相关产品推荐

