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

SwiftUI中如何异步预加载并显示PDF首页图片?

解决SwiftUI中PDF首页预加载卡顿问题(异步加载+加载状态显示)

一、重构ViewModel实现异步图片加载

核心是将PDF转图片的耗时操作移至后台线程执行,避免阻塞主线程;同时优化重复创建PDFDocument的问题,减少资源消耗。

修改后的FileInfoViewModel代码:

struct PDFFileInfo: Identifiable {
    let id = UUID()
    let name: String
    let pageCount: Int
    let creationDate: Date
    let fileURL: URL
}

class FileInfoViewModel: ObservableObject {
    
    @Published var pdfFiles: [PDFFileInfo] = []
    @Published var preloadedImages: [String: UIImage] = [:]
    // 记录正在加载的文件,避免重复发起请求
    private var loadingFiles: Set<String> = []
    // 后台队列,用于处理耗时IO与绘制操作
    private let backgroundQueue = DispatchQueue(label: "com.yourapp.pdf-image-loader", qos: .userInitiated)

    init() {
        fetchPDFFiles()
    }
    
    func fetchPDFFiles() {
        guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
        
        do {
            let fileURLs = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: [.creationDateKey], options: [])
            
            var tempFiles: [PDFFileInfo] = []
            
            for fileURL in fileURLs where fileURL.pathExtension.lowercased() == "pdf" {
                guard let pdfDocument = PDFDocument(url: fileURL),
                      let creationDate = try? fileURL.resourceValues(forKeys: [.creationDateKey]).creationDate else {
                    continue
                }
                
                let fileInfo = PDFFileInfo(name: fileURL.lastPathComponent, pageCount: pdfDocument.pageCount, creationDate: creationDate, fileURL: fileURL)
                tempFiles.append(fileInfo)
                
                // 异步加载当前PDF首页图片
                loadFirstPageImage(for: fileURL)
            }
            
            // 主线程更新文件列表并排序
            DispatchQueue.main.async {
                self.pdfFiles = tempFiles.sorted(by: { $0.creationDate > $1.creationDate })
            }
            
        } catch {
            print("获取PDF文件出错: \(error)")
        }
    }
    
    private func loadFirstPageImage(for fileURL: URL) {
        let fileName = fileURL.lastPathComponent
        // 跳过已加载或正在加载的文件
        guard !loadingFiles.contains(fileName), preloadedImages[fileName] == nil else { return }
        
        loadingFiles.insert(fileName)
        
        backgroundQueue.async { [weak self] in
            guard let self = self,
                  let pdfDocument = PDFDocument(url: fileURL),
                  let firstPage = pdfDocument.page(at: 0) else {
                self?.loadingFiles.remove(fileName)
                return
            }
            
            let image = self.firstPageToImage(firstPage)
            
            // 回到主线程更新UI状态
            DispatchQueue.main.async {
                self.preloadedImages[fileName] = image
                self.loadingFiles.remove(fileName)
            }
        }
    }
    
    private func firstPageToImage(_ page: PDFPage) -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: page.bounds(for: .mediaBox).size)
        return renderer.image { context in
            page.draw(with: .mediaBox, to: context.cgContext)
        }
    }
}

二、修改View层,显示加载状态

调整列表渲染逻辑,确保每个文件cell都能显示,未加载完成时展示ProgressView,加载完成后显示图片;同时重构FileCellView的参数传递逻辑。

1. 列表渲染代码修改

ForEach(fileInfoViewModel.pdfFiles, id: \.id) { file in
    FileCellView(
        image: fileInfoViewModel.preloadedImages[file.name],
        title: file.name,
        dateOfCreation: dateFormatterStore.dateFormatter.string(from: file.creationDate),
        fileSize: fileSizeInMB(file.fileURL)
    ) {
        // 编辑操作逻辑
    } delete: {
        // 删除操作逻辑
    } share: {
        // 分享操作逻辑
    }
}

2. 重构FileCellView

struct FileCellView: View {
    
    let image: UIImage?
    let title: String
    let dateOfCreation: String
    let fileSize: String
    
    let edit: () -> Void
    let delete: () -> Void
    let share: () -> Void
    
    var body: some View {
        HStack {
            // 图片或加载指示器区域
            Group {
                if let image = image {
                    Image(uiImage: image)
                        .resizable()
                        .scaledToFit()
                        .cornerRadius(4)
                } else {
                    ProgressView()
                        .frame(width: 80, height: 100)
                }
            }
            .padding(.trailing)
            
            // 文件信息展示区域
            VStack(alignment: .leading, spacing: 8) {
                Text(title)
                    .foregroundColor(.white)
                    .font(.headline)
                Text(dateOfCreation)
                    .foregroundColor(.gray)
                    .font(.caption)
                Text(fileSize)
                    .foregroundColor(.gray)
                    .font(.caption)
            }
            
            Spacer()
            
            // 操作按钮区域
            HStack(spacing: 16) {
                Button(action: edit) {
                    Image(systemName: "pencil")
                        .foregroundColor(.white)
                }
                Button(action: delete) {
                    Image(systemName: "trash")
                        .foregroundColor(.red)
                }
                Button(action: share) {
                    Image(systemName: "square.and.arrow.up")
                        .foregroundColor(.white)
                }
            }
        }
        .frame(height: 120)
        .padding(15)
        .background(Color(red: 0.14, green: 0.14, blue: 0.14))
        .cornerRadius(12)
    }
}

三、关键优化点说明

  • 异步线程隔离:将PDF解析、图片绘制等耗时操作移至后台队列,主线程仅负责UI更新,彻底解决启动/加载时的卡顿问题。
  • 重复加载防护:通过loadingFiles集合跟踪正在加载的文件,避免同一文件重复发起加载请求。
  • 资源复用优化:修复原代码中重复创建PDFDocument的问题,每个PDF仅解析一次,减少内存占用与IO开销。
  • 状态统一管理:ViewModel集中管理图片加载状态,View层只需根据状态展示对应内容,逻辑清晰易维护。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 19:50:32