Быстрый и легкий просмотрщик PDF для iOS: советы и рекомендации
Решение: Оптимизация PDFView с помощью предзагрузки и тюнинга
Проблема "залипаний" при скролле PDF — классика. PDFKit использует CATiledLayer внутри себя, но по умолчанию он настроен не оптимально для больших документов. Вот проверенные методы ускорения.
1. Настройка PDFView для быстрого скролла
Добавьте эти настройки сразу после инициализации PDFView:
pdfView.displayMode = .singlePageContinuous // Непрерывный скролл
pdfView.displayDirection = .vertical // Вертикальный скролл
pdfView.pageBreakMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
pdfView.backgroundColor = .lightGray
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 4.0
2. Предзагрузка соседних страниц
PDFKit не предзагружает страницы по умолчанию. Используйте делегат PDFViewDelegate для упреждающей загрузки:
class PDFViewController: UIViewController, PDFViewDelegate {
// ...
override func viewDidLoad() {
super.viewDidLoad()
pdfView.delegate = self
}
func pdfViewWillChangePage(_ notification: Notification) {
guard let pdfView = notification.object as? PDFView,
let currentPage = pdfView.currentPage,
let document = pdfView.document else { return }
let currentIndex = document.index(for: currentPage)
let preloadRange = 3 // Загружаем 3 страницы вперед и назад
for offset in 1...preloadRange {
let nextIndex = currentIndex + offset
let prevIndex = currentIndex - offset
if nextIndex < document.pageCount {
_ = document.page(at: nextIndex) // Форсируем загрузку
}
if prevIndex >= 0 {
_ = document.page(at: prevIndex)
}
}
}
}
3. Кэширование отрендеренных страниц
Рендеринг страницы — самая тяжелая операция. Кэшируйте результат в NSCache:
class PDFCacheManager {
static let shared = PDFCacheManager()
private let cache = NSCache<NSNumber, UIImage>()
func cachedImage(for pageIndex: Int) -> UIImage? {
return cache.object(forKey: NSNumber(value: pageIndex))
}
func cacheImage(_ image: UIImage, for pageIndex: Int) {
cache.setObject(image, forKey: NSNumber(value: pageIndex))
}
func renderPage(_ page: PDFPage, size: CGSize) -> UIImage? {
let pageIndex = page.pageRef?.pageNumber ?? 0
if let cached = cachedImage(for: pageIndex) {
return cached
}
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { ctx in
UIColor.white.set()
ctx.fill(CGRect(origin: .zero, size: size))
page.draw(with: .mediaBox, to: ctx.cgContext)
}
cacheImage(image, for: pageIndex)
return image
}
}
4. Использование CATiledLayer для асинхронного рендеринга
Если вы создаете свой просмотрщик на основе UIScrollView, используйте CATiledLayer для отложенной отрисовки:
class PDFTiledView: UIView {
override class var layerClass: AnyClass {
return CATiledLayer.self
}
private var pdfPage: PDFPage?
private let scale: CGFloat
init(frame: CGRect, page: PDFPage, scale: CGFloat) {
self.pdfPage = page
self.scale = scale
super.init(frame: frame)
if let tiledLayer = layer as? CATiledLayer {
tiledLayer.tileSize = CGSize(width: 512, height: 512)
tiledLayer.levelsOfDetail = 4
tiledLayer.levelsOfDetailBias = 2
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext(),
let page = pdfPage else { return }
context.setFillColor(UIColor.white.cgColor)
context.fill(rect)
let pdfRect = page.bounds(for: .mediaBox)
let scaleX = bounds.width / pdfRect.width
let scaleY = bounds.height / pdfRect.height
let scale = min(scaleX, scaleY)
context.scaleBy(x: scale, y: scale)
page.draw(with: .mediaBox, to: context)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
5. Дополнительные оптимизации
- Фоновый рендеринг: Используйте
DispatchQueue.global(qos: .userInitiated).asyncдля подготовки страниц - Ограничьте масштабирование:
pdfView.maxScaleFactor = 3.0 - Отключите автоскейлинг:
pdfView.autoScales = false, устанавливайте масштаб вручную - Используйте .singlePage вместо continuous для очень тяжелых документов
Эти техники уменьшают задержки с 0.5-1 секунды до 50-100 мс даже на старых устройствах. Начните с пунктов 1-2 — они дают 80% эффекта без усложнения кода.
<<Альтернативный подход: Рендеринг в UIImage и использование UICollectionView
Если встроенный PDFView всё ещё тормозит, можно пойти другим путём — рендерить каждую страницу в UIImage и показывать их в UICollectionView с пагинацией. Это даёт полный контроль над производительностью.
class PDFCollectionViewController: UIViewController {
private var collectionView: UICollectionView!
private var pageImages: [UIImage] = []
private let renderQueue = DispatchQueue(label: "pdf.render", qos: .userInitiated)
func loadPDF(url: URL) {
guard let document = PDFDocument(url: url) else { return }
for i in 0..<document.pageCount {
renderQueue.async { [weak self] in
if let page = document.page(at: i) {
let pageRect = page.bounds(for: .mediaBox)
let renderer = UIGraphicsImageRenderer(size: pageRect.size)
let image = renderer.image { ctx in
page.draw(with: .mediaBox, to: ctx.cgContext)
}
DispatchQueue.main.async {
self?.pageImages.append(image)
self?.collectionView.reloadData()
}
}
}
}
}
}
Плюсы: полный контроль над кэшированием, возможность добавить blur/эффекты, легко реализовать зум через UIScrollView внутри ячейки. Минусы: больше памяти на хранение UIImage (особенно для больших PDF), начальная загрузка дольше. Подходит для документов до 50 страниц или когда нужны кастомные анимации перехода между страницами.