Amsterdam: Working with PDF – rendering to image, move to page

Hi folks, I'm at home in Amsterdam and it's been gray and a little cold. It's snowing, but only slightly. Every winter I am hoping for a strict winter like they easily seemed to occur  every year when I was a kid. For weather forcasts the weatherman used simple air pressure maps, not computed models which can predict the weather 14 days ahead. These today-models are 85 % right for the next 5 days. That's why there's no room for surprises like in 1979. I was 7 years old and our house was totally covered in snow. My dad climbed out of the window to create a path. He made a huge pile of the snow he scooped away. I took my little shovel, hollowed out the pile, and so created my very own, ridiculously professional iglo! Well sweet memories, I guess I'm getting old, haha! Let's go back to code and talk a little more about PDF.

Rendering PDF

In a previous post I showed you a simple but adequate way to display a PDF in a webview. That was nice, but you probably noticed it was also a bit slow. On top of that; once the PDF was there, we could look at it, but that was all. There are other, more advanced ways to handle PDF, but this requires a little more code. In the code beneath I created a viewController to not only view a PDF, but also page through the file by swiping left and right. On top of that I created a way to fill in a pagenumber and imediately move to the page with on click of a button. I didn't use Storyboards, so all the views are in code. You'll see the result beneath:

import UIKit

class PDFToImageVC: UIViewController {

    //create all views needed in this project
    let imgView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFit
        iv.translatesAutoresizingMaskIntoConstraints = false
        iv.isUserInteractionEnabled = true
        return iv
    }()
    
    let textfield: UITextField = {
        let tf = UITextField()
        tf.layer.borderColor = UIColor.lightGray.cgColor
        tf.layer.borderWidth = 0.6
        tf.layer.cornerRadius = 4
        tf.translatesAutoresizingMaskIntoConstraints = false
        tf.textAlignment = .center
        return tf
    }()
    
    let pagesLabel: UILabel = {
        let pl = UILabel()
        pl.font = UIFont.systemFont(ofSize: 14)
        pl.textColor = .lightGray
        pl.translatesAutoresizingMaskIntoConstraints = false
        return pl
    }()
    
    let moveToPageButton: UIButton = {
        let bt = UIButton(type: .system)
        bt.titleLabel?.font = UIFont.boldSystemFont(ofSize: 13)
        bt.setTitle("Move to page", for: .normal)
        bt.layer.borderColor = UIColor.black.cgColor
        bt.layer.borderWidth = 1.0
        bt.layer.cornerRadius = 4
        bt.layer.masksToBounds = true
        bt.backgroundColor = .red
        bt.tintColor = .white
        bt.translatesAutoresizingMaskIntoConstraints = false
        bt.addTarget(self, action: #selector(skipToPage), for: .touchUpInside)
        return bt
    }()
    
    //I create a variable called currentPage. Every time it is set it calls the function drawPDFFromUrl() to render the requested page of the PDF you want to display.
    var currentPage = 0 {
        didSet {
            guard let path = Bundle.main.path(forResource: "Pro Swift", ofType: "pdf") else { return }
            let url = URL(fileURLWithPath: path)
            imgView.image = drawPDFFromUrl(url: url, page: currentPage)
        }
    }
    
    // A variable for the number of pages in the PDF. This variable is set form the drawPDFFromUrl function with .numberOfPages property of the CGPDFDocument.
    var numberOfPages = 0 {
        didSet {
            pagesLabel.text = " / (numberOfPages)"
        }
    }
    
    var textFieldDelegateClass = TextfieldDelegate()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        textfield.keyboardType = .phonePad
        textfield.delegate = textFieldDelegateClass
        textFieldDelegateClass.vc = self
        
        setUpPDFView()
        currentPage = 1
        
        //To dismiss keyboard at tap outside
        let tap = UITapGestureRecognizer(target: self, action: #selector(tapToDismiss))
        view.addGestureRecognizer(tap)

    }
    
    override func viewWillAppear(_ animated: Bool) {
        currentPage = 1
        
    }
    
    //To give all the views proper layouts and add them to the view.
    func setUpPDFView() {
        view.addSubview(textfield)
        textfield.topAnchor.constraint(equalTo: view.topAnchor, constant: 10).isActive = true
        textfield.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        textfield.widthAnchor.constraint(equalToConstant: 40).isActive = true
        textfield.heightAnchor.constraint(equalToConstant: 25).isActive = true
        
        view.addSubview(pagesLabel)
        pagesLabel.topAnchor.constraint(equalTo: textfield.topAnchor).isActive = true
        pagesLabel.leftAnchor.constraint(equalTo: textfield.rightAnchor, constant: 5).isActive = true
        pagesLabel.widthAnchor.constraint(equalToConstant: 100).isActive = true
        pagesLabel.heightAnchor.constraint(equalToConstant: 25).isActive = true
        
        view.addSubview(moveToPageButton)
        moveToPageButton.topAnchor.constraint(equalTo: textfield.bottomAnchor, constant: 15).isActive = true
        moveToPageButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        moveToPageButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
        moveToPageButton.heightAnchor.constraint(equalToConstant: 25).isActive = true
        
        view.addSubview(imgView)
        imgView.topAnchor.constraint(equalTo: moveToPageButton.bottomAnchor, constant: 15).isActive = true
        imgView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        imgView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        imgView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        
        let swipeGestureLeft = UISwipeGestureRecognizer(target: self, action: #selector(swipe))
        swipeGestureLeft.direction = .left
        view.addGestureRecognizer(swipeGestureLeft)
        
        let swipeGestureRight = UISwipeGestureRecognizer(target: self, action: #selector(swipe))
        swipeGestureRight.direction = .right
        view.addGestureRecognizer(swipeGestureRight)
        
        edgesForExtendedLayout = []
    }
    
    //This method renders a PDF-page from a url and gives back a UIImage to set on the imgView. We scale it wit 1, -1, otherwise it would be displayed upside down.
    func drawPDFFromUrl(url: URL, page: Int) -> UIImage? {
        guard let document = CGPDFDocument(url as CFURL) else { return nil }
        guard let page = document.page(at: page) else { return nil }
        numberOfPages = document.numberOfPages
        
        
        let pageRect = page.getBoxRect(.mediaBox)
        let renderer = UIGraphicsImageRenderer(size: pageRect.size)
        let image = renderer.image { ctx in
            UIColor.white.set()
            ctx.fill(pageRect)
            ctx.cgContext.translateBy(x: 0.0, y: pageRect.size.height)
            ctx.cgContext.scaleBy(x: 1, y: -1)
            ctx.cgContext.drawPDFPage(page)
        }
        return image
    }
    
    //A SwipegestureRecognizer detects left and right swipes. We switch on the direction and increase or decrease the pagenumber, forcing to call the drawPDFFromUrl function (remember the didSet on the currentPage-variable!)
    func swipe(gesture: UIGestureRecognizer) {
        if let swipeGesture = gesture as? UISwipeGestureRecognizer {
            switch swipeGesture.direction {
            case UISwipeGestureRecognizerDirection.left:
                if currentPage < numberOfPages {
                    currentPage += 1
                }
            case UISwipeGestureRecognizerDirection.right:
                if currentPage > 1 {
                    currentPage -= 1
                }
            default:
                break
            }
        }
    }
    
    func tapToDismiss() {
        textfield.endEditing(true)
    }
    
    func showAlert(withTitle: String, andText: String) {
        let alert = UIAlertController(title: withTitle, message: andText, preferredStyle: .alert)
        let action = UIAlertAction(title: "Ok", style: .cancel, handler: nil)
        alert.addAction(action)
        present(alert, animated: true, completion: nil)
    }
    //This function is called from a tap on the moveToPageButton. After checking if the textfield was not empty, and if the pagenumber submitted was not bigger than the amount of pages of the document, the drawPDFFromUrl function is called with the submitted number.
    func skipToPage() {
        if textfield.text != "" {
            if Int(textfield.text!)! <= numberOfPages {
                currentPage = Int(textfield.text!)!
                textfield.resignFirstResponder()
            } else {
                showAlert(withTitle: "Page doesn't exist", andText: "This document has got only (numberOfPages) pages. Please enter a valid pagenumber!")
            }
            textfield.text = nil
        }
    }
}

I hope you enjoyed it, I certainly did!

Happy coding, happy traveling!

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *