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