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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 10:13:15