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
相关产品推荐
相关产品推荐

