Avatar ·

Быстрый и легкий просмотрщик PDF для iOS: советы и рекомендации

Всем привет! Я разрабатываю приложение для iOS, в котором нужно отображать PDF-документы (руководства пользователя, каталоги). Проблема в том, что файлы бывают большими — до 100-200 МБ, и стандартный UIScrollView с UIWebView тормозит, а иногда и вылетает. Я пробовал использовать PDFKit, но заметил, что при скролле страниц с большим количеством векторной графики (схемы, чертежи) наблюдается заметная задержка. Вот мой текущий код: ```swift import PDFKit class PDFViewController: UIViewController { var pdfView: PDFView! var document: PDFDocument? override func viewDidLoad() { super.viewDidLoad() pdfView = PDFView(frame: view.bounds) pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(pdfView) if let url = Bundle.main.url(forResource: "manual", withExtension: "pdf") { document = PDFDocument(url: url) pdfView.document = document pdfView.autoScales = true } } } ``` Всё работает, но при пролистывании страниц с большими схемами (особенно если на странице много линий и текста) происходит "залипание" на 0.5-1 секунду. Я слышал, что можно использовать CATiledLayer для рендеринга, но не понимаю, как это применить к PDF. Подскажите, как можно ускорить отображение PDF, особенно для больших документов? Может, есть какие-то best practices по кэшированию или предзагрузке страниц? Заранее спасибо! <<>>

Решение: Оптимизация 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 страниц или когда нужны кастомные анимации перехода между страницами.