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

Xcode UIKit TableViewController无法显示Cloud Firestore数据求助

问题:Firestore数据获取成功但TableView无法渲染单元格

原本App运行正常,将TableView(每个单元格包含图片、标题和副标题,点击可流式播放视频)的数据改为从Cloud Firestore获取后,ViewModel的fetchData方法能成功获取数据(打印data可见内容),但TableView的viewModel.videos.count始终为0,无法渲染单元格。

相关代码

ListOfVideoLessonsTableViewController.swift

import UIKit
import AVKit
import AVFoundation
import FirebaseFirestore
import SwiftUI

class ListOfVideoLessonsTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    let pinchGesture = UIPinchGestureRecognizer()
    
    @ObservedObject private var viewModel = VideosViewModel()
    var player = AVPlayer()
    var playerViewController = AVPlayerViewController()
   
    @IBOutlet var table: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.viewModel.fetchData()
        self.title = "Video Lessons"
        
        table.delegate = self
        table.dataSource = self
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        print("videos count = ", viewModel.videos.count)
        return viewModel.videos.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let video = viewModel.videos[indexPath.row]
        tableView.tableFooterView = UIView()
        
        //configure cell
        cell.textLabel?.text = video.name
        cell.detailTextLabel?.text = video.lessonName
        cell.accessoryType = .disclosureIndicator
        let imageName = UIImage(named: video.imageName)
        cell.imageView?.image = imageName!.withRenderingMode(.alwaysOriginal)
        let backgroundView = UIView()
        backgroundView.backgroundColor = UIColor(named: "VideoLessonsCellHighlighted")
        cell.selectedBackgroundView = backgroundView
        cell.textLabel?.font = UIFont(name: "Helvetica-Bold", size: 14)
        cell.detailTextLabel?.font = UIFont(name: "Helvetica", size: 12)
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        playVideo(at: indexPath)
    }
    
    func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
        if let cell = tableView.cellForRow(at: indexPath) {
            cell.contentView.backgroundColor = UIColor(named: "VideoLessonsCellHighlighted")
            cell.textLabel?.highlightedTextColor = UIColor(named: "textHighlighted")
            cell.detailTextLabel?.highlightedTextColor = UIColor(named: "textHighlighted")
        }
    }

    func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
        if let cell = tableView.cellForRow(at: indexPath) {
            cell.contentView.backgroundColor = nil
        }
    }

    func playVideo(at indexPath: IndexPath){
        let selectedVideo = viewModel.videos[indexPath.row]
        let url = URL(string: selectedVideo.fileURL)
        player = AVPlayer(url: url!)
        playerViewController.player = player
        
        self.present(playerViewController, animated: true, completion: {
            self.playerViewController.player?.play()
        })
    }
}

VideosViewModel.swift

import Foundation
import FirebaseAnalytics
import FirebaseFirestore

class VideosViewModel: ObservableObject {
    @Published var videos = [Video]()
    
    private var db = Firestore.firestore()
    
    func fetchData() {
        db.collection("videos").addSnapshotListener { [self] (querySnapshot, error) in
            guard let documents = querySnapshot?.documents else {
                print("No Documents")
                return
            }
            
            self.videos = documents.map { (queryDocumentSnapshot) -> Video in
               let data = queryDocumentSnapshot.data()
                
                let name = data["name"] as? String ?? ""
                let imageName = data["imageName"] as? String ?? ""
                let lessonName  = data["lessonName"] as? String ?? ""
                let fileURL  = data["fileURL"] as? String ?? ""
        
                print(data)
                return Video(name: name, imageName: imageName, lessonName: lessonName, fileURL: fileURL)
            }
        }
    }
}

解决方案

1. 适配UIKit中的ObservableObject监听

UIKit不会像SwiftUI那样自动响应ObservableObject的变化,需要手动订阅@Published属性的更新:

  • 导入Combine框架:
import Combine
  • 在ViewController中添加存储订阅的属性:
private var cancellables = Set<AnyCancellable>()
  • 将@ObservedObject替换为@StateObject(iOS 14及以上版本推荐):
@StateObject private var viewModel = VideosViewModel()
  • 在viewDidLoad中添加监听,触发TableView刷新:
override func viewDidLoad() {
    super.viewDidLoad()
    
    self.viewModel.fetchData()
    self.title = "Video Lessons"
    
    table.delegate = self
    table.dataSource = self
    
    // 监听videos数组变化,主线程更新TableView
    viewModel.$videos
        .receive(on: DispatchQueue.main)
        .sink { [weak self] _ in
            self?.table.reloadData()
        }
        .store(in: &cancellables)
}

2. 确保Firestore回调在主线程更新数据(可选但稳妥)

虽然@Published会自动切换到主线程,但手动在Firestore回调中切换到主线程更新videos数组,可避免潜在线程问题:

func fetchData() {
    db.collection("videos").addSnapshotListener { [self] (querySnapshot, error) in
        guard let documents = querySnapshot?.documents else {
            print("No Documents")
            return
        }
        
        DispatchQueue.main.async {
            self.videos = documents.map { (queryDocumentSnapshot) -> Video in
               let data = queryDocumentSnapshot.data()
                
                let name = data["name"] as? String ?? ""
                let imageName = data["imageName"] as? String ?? ""
                let lessonName  = data["lessonName"] as? String ?? ""
                let fileURL  = data["fileURL"] as? String ?? ""
        
                print(data)
                return Video(name: name, imageName: imageName, lessonName: lessonName, fileURL: fileURL)
            }
        }
    }
}

3. 验证Video模型字段匹配

确认Firestore集合中的字段名(name、imageName、lessonName、fileURL)与ViewModel中解析的字段完全一致,避免因字段名不匹配导致的空值或未正确初始化Video对象。

原因说明

问题核心在于UIKit不具备SwiftUI的自动状态更新机制:当ViewModel的videos数组被Firestore数据更新后,TableView没有收到任何刷新通知,因此始终显示初始的count=0。通过手动订阅@Published属性的变化,调用table.reloadData(),就能让TableView响应数据更新并渲染单元格。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 16:10:32