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

Firestore加载数据时Optional解包崩溃及计数异常求助

问题解决方案

问题概述

视频列表TableViewController可正常显示name和lessonName字段,但解包imageName对应的UIImage、fileURL对应的URL时触发「Found nil while unwrapping optional」崩溃;控制台多次打印videos count,先输出3次videos count = 0,后输出3次videos count = 2,存在数据加载时序或代码逻辑问题。

报错根源分析

  1. 资源路径不匹配:从日志可见imageName和fileURL是Firebase Storage的gs://协议路径,而非本地资源名或标准HTTP/HTTPS URL,直接用UIImage(named:)和URL(string:)无法解析,返回nil后强制解包导致崩溃。
  2. ViewModel数组赋值逻辑错误:在fetchData的documents.map闭包中错误调用videos.append,导致videos数组被重复赋值,触发多次@Published回调,进而引发TableView多次reload,出现重复的count打印。
  3. 无容错的强制解包:所有可能返回nil的操作(如图片加载、URL解析)未做可选绑定,直接使用!强制解包,一旦值为nil就触发崩溃。

分步解决方案

1. 修复ViewModel数组赋值逻辑

移除map闭包内的videos.append调用,避免数组重复赋值,减少不必要的TableView reload触发。

修改后的VideosViewModel代码:

import Foundation
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)
            }
        }
    }
}

2. 正确加载Firebase Storage资源

gs://路径是Firebase Storage的内部标识,需通过Firebase Storage SDK获取下载URL后再加载资源:

加载列表项图片

在cellForRowAt中替换原图片加载代码,使用Storage SDK异步获取图片并加载:

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()
    
    // 基础配置保持不变
    cell.textLabel?.text = video.name
    cell.detailTextLabel?.text = video.lessonName
    cell.accessoryType = .disclosureIndicator
    
    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)
    
    // 异步加载Firebase Storage图片
    let storageRef = Storage.storage().reference(forURL: video.imageName)
    storageRef.downloadURL { url, error in
        if let error = error {
            print("获取图片下载URL失败:\(error.localizedDescription)")
            return
        }
        guard let imageURL = url else {
            print("图片URL无效")
            return
        }
        
        URLSession.shared.dataTask(with: imageURL) { data, _, error in
            if let error = error {
                print("下载图片失败:\(error.localizedDescription)")
                return
            }
            guard let imageData = data, let image = UIImage(data: imageData) else {
                print("无法解析图片数据")
                return
            }
            
            DispatchQueue.main.async {
                cell.imageView?.image = image.withRenderingMode(.alwaysTemplate)
            }
        }.resume()
    }
    
    return cell
}

加载播放视频

修改playVideo方法,先获取视频的下载URL再初始化AVPlayer:

func playVideo(at indexPath: IndexPath){
    let selectedVideo = viewModel.videos[indexPath.row]
    let storageRef = Storage.storage().reference(forURL: selectedVideo.fileURL)
    
    storageRef.downloadURL { [weak self] url, error in
        guard let self = self else { return }
        
        if let error = error {
            print("获取视频下载URL失败:\(error.localizedDescription)")
            return
        }
        guard let videoURL = url else {
            print("视频URL无效")
            return
        }
        
        let player = AVPlayer(url: videoURL)
        self.playerViewController.player = player
        self.present(self.playerViewController, animated: true) {
            player.play()
        }
    }
}

3. 移除所有强制解包操作

确保所有可能返回nil的操作都通过if let或guard let做可选绑定,彻底避免解包崩溃。

控制台count打印问题说明

原代码中ViewModel的videos.append导致数组被重复赋值,加上Firestore snapshotListener首次可能返回空文档、TableView初始加载的多次调用,引发多次count打印。修复ViewModel的赋值逻辑后,videos只会在数据更新时被正确赋值一次,TableView reload次数会恢复正常,count打印次数也会减少。

内容的提问来源于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 17:30:55