8
votes

I have an iPad application which I've successfully moved to Mac using Catalyst.

While I can generate PDFs on the iPad/iPhone using UIMarkupTextPrintFormatter, it doesn't work on the Mac when it really should.

In fact, I cannot even build the Mac binary unless I comment out UIMarkupTextPrintFormatter using #if !targetEnvironment(macCatalyst) as Xcode simply presents an error:

Undefined symbols for architecture x86_64:
"_OBJC_CLASS_$_UIMarkupTextPrintFormatter", referenced from: objc-class-ref in Functions.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)

It's confusing as Apple's documentation suggests it is compatible with Mac Catalyst 13.0+ https://developer.apple.com/documentation/uikit/uimarkuptextprintformatter

Has anyone else experienced this and were you able to find a solution?

Thank you.

EDIT: I have found an excellent solution which also works without modification in macCatalyst, based on Sam Wize's post here:

https://samwize.com/2019/07/02/how-to-generate-pdf-with-images/

The key is to use a WKWebView object (but not show it) as an intermediary to load the HTML file, then use it's viewPrintFormatter to render a PDF via its didFinish navigation: delegate

Here is my code (hopefully the comments are self explanatory). Create a a Swift file called PDFCreator.swift with the following code:

import WebKit

typealias PDFCompletion = (Result<NSData, Error>) -> Void

class PDFCreator: NSObject {
var webView: WKWebView? = nil
var completion: PDFCompletion!

func exportPDF(html: String, completion: @escaping PDFCompletion) throws {
    // Set up the completion handler to be called by the function in the delegate method
    // It has to be instantiated here so the delegate method can access it
    self.completion = completion
    // Creates a WebKit webView to load the HTML string & sets the delegate (self) to respond
    let webView = WKWebView()
    webView.navigationDelegate = self
    // If the other assets are in the same baseURL location (eg. Temporary Documents Directory, they will also render)
    // But you need to ensure the assets are already there before calling this function
    let baseURL = URL(fileURLWithPath: NSTemporaryDirectory())
    // Loads the HTML string into the WebView and renders it (invisibly) with any assets
    webView.loadHTMLString(html, baseURL: baseURL)
    self.webView = webView
    // After this function closes, the didFinish navigation delegate method is called
    }


func createPDF(_ formatter: UIViewPrintFormatter) {
    // Subclass UIPrintPageRenderer if you want to add headers/footers, page counts etc.
    let printPageRenderer = UIPrintPageRenderer()
    printPageRenderer.addPrintFormatter(formatter, startingAtPageAt: 0)

    // Assign paperRect and printableRect
    // A4, 72 dpi
    let paperRect = CGRect(x: 0, y: 0, width: 595.2, height: 841.8)
    let padding: CGFloat = 20
    let printableRect = paperRect.insetBy(dx: padding, dy: padding)
    printPageRenderer.setValue(printableRect, forKey: "printableRect")
    printPageRenderer.setValue(paperRect, forKey: "paperRect")
    // Assign header & footer dimensions
    printPageRenderer.footerHeight = 70
    printPageRenderer.headerHeight = 20

    // Create PDF context and draw
    let pdfData = NSMutableData()
    UIGraphicsBeginPDFContextToData(pdfData, .zero, nil)
    for i in 0..<printPageRenderer.numberOfPages {
        UIGraphicsBeginPDFPage();
        printPageRenderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
    }
    UIGraphicsEndPDFContext();

    // Send the PDF data out with a Result of 'success' & the NSData object for processing in the completion block
    self.completion?(.success(pdfData))
    }
}


extension PDFCreator: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    let viewPrintFormatter = webView.viewPrintFormatter()
    createPDF(viewPrintFormatter)
    }
}

In my App I instantiate a PDFCreator object

let pdfCreator = PDFCreator()

Then I ensure all the local assets needed for the HTML file are created first in the same 'baseURL' location - in my case the NSTemporaryDirectory() - then run the following:

let pdfFilePath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("test.pdf")

 try? pdfCreator.exportPDF(html: htmlString, completion: { (result) in
         switch result {
         case .success(let data):
                try? data.write(to: pdfFilePath, options: .atomic)
                // *** Do stuff with the file at pdfFilePath ***

         case .failure(let error):
                print(error.localizedDescription)
            }
        })
1
I’ve also filed a bug report with Apple so we’ll see what they say.Paul Martin
Upvoted, as I have the exact same problem. Thanks for the suggestion on how to comment it out. Sadly I don't haven't found any solution yet so it may indeed be an Apple bug.kvaruni
Thanks. As soon as I have an answer to this I’ll post it here!Paul Martin
Still not fixed with 13.3 and Xcode 11.3 :-/Paul Martin
Found a solution (see edit above). It's WAY more elegant and works with macCatalyst and produces PDFs from HTML, with images!Paul Martin

1 Answers

2
votes

I have the same problem. But I was able to get around it by using Swift's function to convert html to attributed text and then use UISimpleTextPrintFormatter with the attributed text.

My original code:

let formatter = UIMarkupTextPrintFormatter(markupText: htmlString)
formatter.perPageContentInsets = UIEdgeInsets(top: 70.0, left: 60.0, bottom: 70.0, right: 60.0)
printController.printFormatter = formatter
printController.present(animated: true, completionHandler: nil)

Working on Catalyst (and iOS):

guard let printData = htmlString.data(using: String.Encoding.utf8) else { return }
do {
    let printText =  try NSAttributedString(data: printData, options: [.documentType: NSAttributedString.DocumentType.html,  .characterEncoding: String.Encoding.utf8.rawValue],  documentAttributes: nil)
        
    let formatter = UISimpleTextPrintFormatter(attributedText: printText)
    formatter.perPageContentInsets = UIEdgeInsets(top: 70.0, left: 60.0, bottom: 70.0, right: 60.0)
    printController.printFormatter = formatter
    printController.present(animated: true, completionHandler: nil)
} catch {
     print(error)
}

However, the NSAttributedString(data: ) seems to be more sensitive to what you throw at it on Catalyst than on iOS. For example, did I have problems with tables that worked fine on iOS. So it is not a perfect solution.

EDIT A better solution that seems to handle e.g. tables better is:

func compHandler(attributedString:NSAttributedString?, attributeKey:[NSAttributedString.DocumentAttributeKey : Any]?, error:Error?) -> Void {
    guard let printText = attributedString else { return }
    let formatter = UISimpleTextPrintFormatter(attributedText: printText)
    formatter.perPageContentInsets = UIEdgeInsets(top: 70.0, left: 60.0, bottom: 70.0, right: 60.0)
    printController.printFormatter = formatter
    printController.present(animated: true, completionHandler: nil)
}
        
guard let printData = htmlString.data(using: String.Encoding.utf8) else { return }
NSAttributedString.loadFromHTML(data: printData, options: [.documentType: NSAttributedString.DocumentType.html,  .characterEncoding: String.Encoding.utf8.rawValue], completionHandler: compHandler)