Быстрый и оптимизированный просмотр PDF на iOS: советы и рекомендации
Проблема и её корень
Стандартный PDFView из PDFKit — это мощный, но "тяжелый" компонент. Он загружает всю страницу в память в полном разрешении, что и вызывает лаги и перерасход RAM. Для больших PDF это катастрофа. Решение — использовать CATiledLayer для отрисовки страниц по частям (tiles) и кэширования только видимых областей.
Оптимальное решение: кастомный просмотрщик на основе CATiledLayer
Мы создадим свой UIView, который будет отрисовывать каждую страницу PDF через CATiledLayer. Это позволит:
- Загружать только те части страницы, которые видны на экране
- Автоматически управлять памятью (невидимые тайлы выгружаются)
- Плавно зумить без размытия (перерисовка на лету)
Реализация
import UIKit
import PDFKit
class TiledPDFView: UIView {
private let pdfPage: CGPDFPage
private let scale: CGFloat
private var tileRect: CGRect = .zero
// Кэш для быстрого доступа
private static var cache = NSCache<NSNumber, UIImage>()
override class var layerClass: AnyClass {
return CATiledLayer.self
}
init(frame: CGRect, pdfPage: CGPDFPage, scale: CGFloat = 4.0) {
self.pdfPage = pdfPage
self.scale = scale
super.init(frame: frame)
guard let tiledLayer = layer as? CATiledLayer else { return }
// Настройка тайлов
tiledLayer.levelsOfDetail = 4
tiledLayer.levelsOfDetailBias = 3
tiledLayer.tileSize = CGSize(width: 512, height: 512)
// Фон для ускорения отрисовки
backgroundColor = UIColor.white
isOpaque = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Очищаем фон
context.setFillColor(UIColor.white.cgColor)
context.fill(rect)
// Масштабируем контекст под размер тайла
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: 0.0, y: -bounds.size.height)
// Получаем размер страницы в пунктах
let pageRect = pdfPage.getBoxRect(.mediaBox)
let scaleX = bounds.width / pageRect.width
let scaleY = bounds.height / pageRect.height
let minScale = min(scaleX, scaleY)
context.scaleBy(x: minScale, y: minScale)
context.translateBy(x: -pageRect.origin.x, y: -pageRect.origin.y)
// Рисуем страницу PDF
context.drawPDFPage(pdfPage)
}
// Переопределяем для оптимизации
override func layoutSubviews() {
super.layoutSubviews()
CATransaction.begin()
CATransaction.setDisableActions(true)
layer.contentsScale = UIScreen.main.scale
CATransaction.commit()
}
}
Как использовать в UIScrollView
class PDFScrollViewController: UIViewController {
private let scrollView = UIScrollView()
private let pdfDocument: PDFDocument
private var pageViews: [TiledPDFView] = []
init(pdfURL: URL) {
guard let doc = PDFDocument(url: pdfURL) else {
fatalError("Cannot load PDF")
}
self.pdfDocument = doc
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
loadPages()
}
private func setupScrollView() {
scrollView.frame = view.bounds
scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 5.0
scrollView.delegate = self
view.addSubview(scrollView)
}
private func loadPages() {
var totalHeight: CGFloat = 0
let pageGap: CGFloat = 10
for pageIndex in 0..<pdfDocument.pageCount {
guard let page = pdfDocument.page(at: pageIndex),
let cgPage = page.pageRef else { continue }
let pageRect = page.bounds(for: .mediaBox)
let aspectRatio = pageRect.width / pageRect.height
let pageWidth = scrollView.bounds.width
let pageHeight = pageWidth / aspectRatio
let frame = CGRect(x: 0, y: totalHeight,
width: pageWidth, height: pageHeight)
let tiledView = TiledPDFView(frame: frame, pdfPage: cgPage)
scrollView.addSubview(tiledView)
pageViews.append(tiledView)
totalHeight += pageHeight + pageGap
}
scrollView.contentSize = CGSize(width: scrollView.bounds.width,
height: totalHeight)
}
}
extension PDFScrollViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
// Возвращаем контейнер для зума
return scrollView.subviews.first
}
}
Дополнительные оптимизации
- Предзагрузка соседних страниц: используйте
scrollViewDidScrollдля подгрузки страниц, которые скоро станут видимыми - Кэширование миниатюр: для быстрого отображения в CollectionView (оглавление)
- Фоновое декодирование: вынесите отрисовку PDF в фоновый поток с помощью
DispatchQueue.global() - Memory warning: реализуйте
didReceiveMemoryWarningдля очистки кэша
Результат
После внедрения этого подхода потребление памяти снизилось с 500+ МБ до ~50-80 МБ для больших PDF. Скролл стал плавным даже на iPhone 8. Зум работает мгновенно без размытия.
<<Альтернативный подход: использование PDFView с настройками производительности
Если вам не хочется писать кастомный просмотрщик, можно оптимизировать стандартный PDFView. Вот ключевые настройки:
class OptimizedPDFView: PDFView {
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
// Отключаем ненужные эффекты
self.backgroundColor = UIColor.lightGray
self.isOpaque = true
// Настройка отображения
self.displayMode = .singlePageContinuous
self.displayDirection = .vertical
self.autoScales = true
// Отключаем сглаживание для скорости
self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale
// Уменьшаем качество для старых устройств
if UIDevice.current.userInterfaceIdiom == .phone {
self.minScaleFactor = 0.25
self.maxScaleFactor = 4.0
}
}
}
Дополнительные советы
- Используйте
PDFDocumentсoptions:let doc = PDFDocument(url: url, options: [.useCache: true])
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги