swiftswiftuipdfkit

How to Efficiently Preload and Display First Page Images of PDF Files in SwiftUI?


I'm working on a SwiftUI app where I have a FileInfoViewModel class responsible for managing PDF files. The class fetches PDF files from the Documents directory, and I'm trying to preload and display the first page of each PDF as an UIImage.

Here's a simplified version of my FileInfoViewModel class:

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

class FileInfoViewModel: ObservableObject {
    
    @Published var pdfFiles: [PDFFileInfo] = [] // Store PDF file info
    @Published var preloadedImages: [String: UIImage] = [:]
    
    init() {
        fetchPDFFiles()
    }
    
    // Function to fetch PDF files from the Documents directory based on date range
    func fetchPDFFiles() {
        if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
            do {
                let fileURLs = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil, options: [])
                
                // Filter and process PDF files within the date range
                for fileURL in fileURLs {
                    if fileURL.pathExtension.lowercased() == "pdf" {
                        if let pdfDocument = PDFDocument(url: fileURL),
                           let creationDate = try? fileURL.resourceValues(forKeys: [.creationDateKey]).creationDate {
                            // Check if the creation date is within the specified date range

                                let fileInfo = PDFFileInfo(name: fileURL.lastPathComponent, pageCount: pdfDocument.pageCount, creationDate: creationDate, fileURL: fileURL)
                                self.pdfFiles.append(fileInfo)
                            }

                              if let pdfDocument = PDFDocument(url: fileURL), let firstPage = pdfDocument.page(at: 0) {
                                   self.preloadedImages[fileURL.lastPathComponent] = self.firstPageToImage(firstPage)
                                }
                            }
                        
                    }
                }
                
                // Sort pdfFiles array by creation date in chronological order
                pdfFiles.sort(by: { $0.creationDate > $1.creationDate })
                
                // Group pdfFiles by creation date
//                groupedPDFFiles = Dictionary(grouping: pdfFiles, by: { Calendar.current.startOfDay(for: $0.creationDate) })
            } catch {
                // Handle error
                print("Error while fetching PDF files: \(error)")
            }
        }
    }
    
    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)
        }
    }
    
}

As you can see, I'm preloading them in this function and appending preloadedImages array. But when my app is starting and when I'm loading files again, I'm encountering lagging during this process. So the question is - how to load them asynchronously, so when it's not ready yet, it can display ProgressView or something like that.

                            ForEach(fileInfoViewModel.pdfFiles, id: \.id) { file in
                                
                                // image: firstPageToImage(firstPage),
                                    if let uiImage = fileInfoViewModel.preloadedImages[file.name] {
                                        

                                        FileCellView(title: file.name, dateOfCreation: dateFormatterStore.dateFormatter.string(from: file.creationDate), fileSize: fileSizeInMB(file.fileURL)) {
                                            
                                        } delete: {
                                            
                                        } share: {

                                        }
                                    }
}
struct FileCellView: View {
    
    @State var image: UIImage? = UIImage(named: "1")!
    @State var title: String = "Scanner 1"
    @State var dateOfCreation: String = "31.02.2023"
    @State var fileSize: String = "132 Gb"
    
    let edit: () -> Void
    let delete: () -> Void
    let share: () -> Void
    
    var body: some View {
        HStack {
            
            if image == image {
                Image(uiImage: image!)
                    .resizable()
                    .scaledToFit()
                    .cornerRadius(4)
                    .padding(.trailing)
            }
            

        }
        .frame(height: 120)
        .padding(15)
        .background(Color(red: 0.14, green: 0.14, blue: 0.14))
        .cornerRadius(12)

    }
}

Solution

  • I've figured out how to lower the CPU consumption.

    // Different method of creating a thumbnail
        private func firstPageToImage(_ page: PDFPage) -> UIImage {
            let cgImage = page.thumbnail(of: CGSize(width: 300, height: 300), for: .mediaBox)
            return cgImage
        }
        
    
    // Different method of preloading images
                    for file in pdfFiles {
                        DispatchQueue.main.async {
                            if let pdfDocument = PDFDocument(url: file.fileURL), let firstPage = pdfDocument.page(at: 0) {
                                self.preloadedImages[file.fileURL.lastPathComponent] = self.firstPageToImage(firstPage)
                            }
                        }
                    }
    
    // And small addition at FileCellView
    struct FileCellView: View {
        
        @EnvironmentObject var fileInfoViewModel: FileInfoViewModel
        
        @State var image: UIImage?
        
        var body: some View {
            HStack {
                
                if fileInfoViewModel.preloadedImages[title] != nil {
                    Image(uiImage: fileInfoViewModel.preloadedImages[title]!)
                        .resizable()
                        .scaledToFit()
                        .cornerRadius(4)
                        .padding(.trailing)
                } else {
                    ProgressView()
                        .padding(.trailing)
                }
          }
    }