3

Note: The question remains unsolved for now; the marked answer provides a good workaround - it works while the application is still open. Answers are still welcomed!

Background

I'm currently developing an app that consists of a full-screen PDFView, and I want the program to remember the position in the document before the view is dismissed so the user can pick up where they've left.

Implementation

A simplified version of the app can be understood as a PDF Viewer using PDFKit.PDFView. The storyboard consists of an UIView that's connected to a PDFView class, DocumentView (which conforms to UIViewController). The view is initialised through the following process:

In viewDidLoad:

let PDF: PDFDocument = GetPDFFromServer()
DocumentView.document = PDF!
DocumentView.autoScales = true
... (other settings)

// Set PDF Destination
let Destination: PDFDestination = GetStoredDestination()
// Code with issues
DocumentView.go(to: Destination)

In viewWillDisappear:

StoreDestination(DocumentView.currentDestination)

The Issue & Tests

I realised that the code does not work as expected; the view does not return to its previous location.

Through debugging, I realised that this might be due to the inconsistent behaviour of DocumentView.go(to destination: PDFDestination) and DocumentView.currentDestination.

To ensure the bug is not introduced by errors while storing the location, the following code is used to verify the issue, with a multi-page document:

In viewDidLoad

Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
    DispatchQueue.main.async {
    
self.DocumentView.go(to:self.DocumentView.currentDestination!)
    }

})

Expected & Observed behaviour

Expected: The location of the document should not change - the code is going to its current destination every 1 second which should have no effects. as "currentDestination" should be the "current destination of the document, per docs")

Observed: Upon execution, the page would spontaneously scroll down by a fixed offset.

The same outcome was observed on an iPadOS 14.5 simulator and an iPadOS 15 iPad Air (Gen 4).

What might have gone wrong?

It'd be great if somebody can help.

Cheers, Lincoln

This question was originally published on the Apple Developer Forum over a week ago; No responses were heard for over a week, so I thought I might try my luck here on StackOverflow <3

Lincoln Yan
  • 337
  • 1
  • 10

1 Answers1

1

I tried PDFView.go() for this scenario and I managed to get it work for some cases but found that it fails in some other scenarios such as with zoomed documents, changed orientations.

So going back to what you are trying to achieve,

I'm currently developing an app that consists of a full-screen PDFView, and I want the program to remember the position in the document before the view is dismissed so the user can pick up where they've left.

this can be done from a different approach. With this approach, you need to always keep a reference to the PDFView you created. If the previous pdf needs to be loaded again, then you pass the PDFView instance you have to the viewController as it is. Otherwise you load the new pdf to the PDFView instance and pass it to the viewController.

DocumentViewController gets the PDFView when it gets initialized.

import UIKit
import PDFKit

protocol DocumentViewControllerDelegate: AnyObject {
    func needsContinuePDF(continuePDF: Bool)
}

class DocumentViewController: UIViewController {
    var pdfView: PDFView!
    weak var delegate: DocumentViewControllerDelegate!
    
    init(pdfView: PDFView, delegate: DocumentViewControllerDelegate){
        self.pdfView = pdfView
        self.delegate = delegate
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func loadView() {
        super.loadView()
        self.view.backgroundColor = .white
        view.addSubview(pdfView)
        NSLayoutConstraint.activate([
            pdfView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            pdfView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            pdfView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            pdfView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        delegate.needsContinuePDF(continuePDF: true)
    }
}

You can initialize DocumentViewController like below. MainViewController has the responsibility for initializing PDFView.

import UIKit
import PDFKit

class MainViewController: UIViewController {
    var pdfView: PDFView = PDFView()
    var continuePreviousPDF = false
    
    let button = UIButton(frame: .zero)
    
    override func loadView() {
        super.loadView()
        
        button.setTitle("View PDF", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .systemBlue
        button.addTarget(self, action: #selector(openDocumentView(_:)), for: .touchUpInside)
        self.view.backgroundColor = .systemGray5
        self.view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.widthAnchor.constraint(equalToConstant: 100),
            button.heightAnchor.constraint(equalToConstant: 50),
            button.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor),
        ])
    }
    
    @objc func openDocumentView(_ sender: UIButton) {
        //open a nee PDF if not continue previous one
        if !self.continuePreviousPDF {
            pdfView.autoScales = true
            pdfView.displayMode = .singlePageContinuous
            pdfView.translatesAutoresizingMaskIntoConstraints = false
            
            guard let path = Bundle.main.url(forResource: "sample copy", withExtension: "pdf") else { return }

            if let document = PDFDocument(url: path) {
                pdfView.document = document
            }
        }
        
        let documentViewController = DocumentViewController(pdfView: pdfView, delegate: self)
        self.present(documentViewController, animated: true, completion: nil)
    }
}

extension MainViewController: DocumentViewControllerDelegate {
    func needsContinuePDF(continuePDF: Bool) {
        self.continuePreviousPDF = continuePDF
    }
}

enter image description here

SamB
  • 1,560
  • 1
  • 13
  • 19
  • Hey SamB, thanks for your brilliant contribution! That worked for me. However, the `pdfView` instance would be destroyed once the application is closed so it would not work after restarting the application. Is there any way to store the destination in UserDefaults (or elsewhere) so that the user can pick up where they've left, after restarting the application? Thanks a lot. – Lincoln Yan Sep 26 '21 at 00:54
  • PDFDestination class cannot be encoded. But you could encode pageNumber, point of the destination and later fetch these from UserDefaults and re-create it as PDFDestination(page: , at: ). However I tried creating a new PDFDestination in your approach in the OP, and tried using it to scroll to the correct position when the pdf loads for the second time. That didn't work perfectly. So I am not sure it would work for this answer. Please note that you have to worry about PDFView.scaleFactor (i.e. zoom in/out) also if you are moving forward with such an approach – SamB Sep 26 '21 at 03:54
  • Thanks, that was my original approach, although it doesn’t seem to be working as expected. I’m suspecting that when calling PDFView.go(to destination: ), the view might attempt to center that destination while PDFView.currentDestination calculates the destination from the bottom left of the page. I’ll try it out & update this answer if there’s any progress - I’m thinking of adding an offset that’s half the height & width of the view. In the meantime, thanks for your help. – Lincoln Yan Sep 26 '21 at 05:47
  • Please post an answer if you could get PDFView.go(to destination) work. I am also now interested in how could that get worked for this scenario :) . Also have a look at PDFView.currentPage, I noticed it could be different to PDFView.currentDestination.page sometimes. – SamB Sep 26 '21 at 06:22
  • It didn't seem like that was the problem. Did a test just now & realized that the actual destination it went using `PDFView.go(to destination:)` was lower than the input destination (i.e. FinalDestination.point.y < Initial.point.y), and the difference in between, `d` depends only on the screen size & not anything else (which document, etc) and is consistent as long as the device is unchanged. I retrieved the offset `d` with a few simulators, plotted it against the size of the view & screen size, and found no relation (was expecting a linear relation). It's just weird - possibly a bug? – Lincoln Yan Sep 26 '21 at 14:26