iOS PDFKit adjust pdf frame in PDFView
Asked Answered
P

4

9

I have a PDF that has pages of different heights that I want to display in a horizontally paged fashion, one page at a time, with no gaps between pages, and the top of each page being aligned with the top of the view. For example:

Desired rendering

I would like to use PDFKit's PDFView, especially since these settings can be achieved by:

let pdfView = PDFView()
pdfView.displayDirection = .horizontal
pdfView.displaysPageBreaks = false
pdfView.usePageViewController(true, withViewOptions: nil)

However, this produces the following:

Actual rendering

This is due to the varying heights of the pages. Each page is centered vertically in the PDFView, and if its height of the page is greater than the height of the view (e.g. Page 2), it is aspect-scaled to fit.

To attempt to solve the first issue, the vertical offset, I have attempted several things. First, and this is gross and I know it's wrong, after inspecting the view hierarchy and finding the private view (a view of class named PDFTextInputView) that the page is rendered within I've tried adjusting its frame's origin.y to 0 whenever the outer-most scrollview (another private view) scrolls. This works in that whenever the next or previous page is scrolling into view, the page is rendered at the top of the view. However, once the user pinches to zoom on that page, its frame is automatically jumps back to being centered. This approach, aside from relying on private view hierarchy, just won't work.

To attempt to solve the "tall page problem" (Page 2) I've tried adjusting the scale factor of the PDFView whenever a new page appears. Sadly, the .PDFViewPageChanged notification only fires after the scroll view ends decelerating, so the page is zoomed out as it's scrolling in and then when I adjust the scale factor it jumps to fit the width of the screen. Then I turned to the .PDFViewVisiblePagesChanged which does fire as the next/previous pages are coming into view (as well as a few more times, which is fine) and this has the same issue as with .PDFViewPageChanged.

Other things I've tried:

  • Adjusting the transform on the document view
  • Intercepting the pinch gesture and adjusting the document view's center
  • Creating my own page controller and render a single PDF page per page (not using pdfView.usePageViewController)

Does anyone have any ideas of how I can achieve what I'm after?

Parliamentary answered 18/5, 2020 at 16:34 Comment(0)
P
5

New Hack:

So it turns out, after digging through some more private APIs (yes, this solution is still gross) that there is this magical property that I had completely overlooked on PDFScrollView (a private, internal view) called... πŸ₯

πŸŽ‰ forcesTopAlignment πŸŽ‰

To enable this, find the PDFScrollView in your PDFView and:

pdfScrollView.setValue(true, forKey: "forcesTopAlignment")

That does the trick!


Old Hack:

After a few days up against the wall I finally figured out a way to get PDFView to behave how I want.

Disclosure: This solution uses method swizzling and relies on private view hierarchy and could break at any time. That said, it works for now and I'm happy with the solution.

The full solution is available in this gist. (The meat is in overrideCenterAlign.)

There is a private method aptly named _centerAlign which vertically centers and scales the pdf pages as they come onto the screen and as they're scaled with the pinch gesture. Swizzling that method allows me to inject my own logic and apply my own transforms to position the pdf view how I'd like (with the top of the page aligned to the top of the view.)

There are two cases to consider. The "short page" case (pages 1, 3, 4 in the example) and the "tall page" case (page 2 in the example.) In both cases I start by invoking the original implementation of _centerAlign so that it can apply its transforms and manage updating the internal scroll view.

  • For the "short page" case, I apply the same transform with a vertical translation to align the top of the pdf with the top of the view.
  • For the "long page" case, I adjust the internal scroll view's zoom scale as it comes onto the screen so that it's scaled to fit the width of the view.

All of that said, I'm open to cleaner solutions that don't rely on private methods and view hierarchy. If you know of a better way to accomplish this, please post an answer!

Parliamentary answered 20/5, 2020 at 17:18 Comment(2)
Surely using this will get your App rejected? – Compton
No, it's in use right now by an app used by millions. – Parliamentary
I
5

Just simply set forcesTopAlignment to true

Example:

lazy var pdfView: PDFView = {
    let pdf = PDFView()
    pdf.displayMode = .singlePageContinuous
    pdf.autoScales = true
    pdf.translatesAutoresizingMaskIntoConstraints = false
    pdf.setValue(true, forKey: "forcesTopAlignment")
    return pdf
}()

How my answer is different from accepted one?

There is no property as a scrollView on pdfView available. So you just set value directly to pdfView.

Infielder answered 11/9, 2020 at 14:23 Comment(0)
P
1

NOTE: this answer is intended to HELP other guys arriving here trying to fix similar problems.

I discovered a nasty bug in iOS12 since apple introduced in iOS13 "drag to hide" (Apple "https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/modality/").

Everything works fine if target is iOS13 or above.

In iOS12, maybe due to a different calculated size, (I guess..) pdf is rendered wrong: some contents go to right and is clipped away.

A dirty fix consists in don't calling

self?.pdfView.autoScales = true

in viewDidLoad,

but after loading every PDF waiting some mS AND set it:

 if #available(iOS 13.0, *) {
    // NADA.. go on..
 }else{
     self.pdfView.autoScales = false // disable it.
     self?.pdfView.alpha = 1
}


let pdf = load ....... 

self.pdfView.document = pdf

 if #available(iOS 13.0, *) {
        //nada..
        } else {
            // hack for ios 12
            let when = DispatchTime.now() + 0.25
            DispatchQueue.main.asyncAfter(deadline: when, execute: { [weak self] in
                self?.pdfView.autoScales = true // hack for ios 12
                self?.pdfView.alpha = 1
            })
        }
  

Use alpha to prevent viewing pdf not adopted yet.

Porterhouse answered 13/6, 2021 at 17:9 Comment(0)
H
0

I think the problem is with adding PDFView to view with constraint. When I added using just addSubview() and user proper frame, it does not require to call setValue.

let pdfView = PDFKit.PDFView(frame: self.view.bounds) pdfView.document = PDFKit.PDFDocument(url: url) pdfView.autoresizingMask = [.flexibleHeight, .flexibleWidth] view.addSubview(pdfView)

Hunan answered 11/12, 2020 at 1:51 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.