9

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)
            }
        })
Paul Martin
  • 699
  • 4
  • 13
  • I’ve also filed a bug report with Apple so we’ll see what they say. – Paul Martin Nov 24 '19 at 11:25
  • 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 Nov 25 '19 at 07:37
  • Thanks. As soon as I have an answer to this I’ll post it here! – Paul Martin Nov 25 '19 at 10:22
  • Still not fixed with 13.3 and Xcode 11.3 :-/ – Paul Martin Dec 11 '19 at 01:25
  • Found a solution (see edit above). It's WAY more elegant and works with macCatalyst and produces PDFs from HTML, with images! – Paul Martin May 11 '20 at 11:09
  • Thanks for the update! This is indeed a good solution with minimal code changes required. You could further improve the answer by making it clear that the trick is to use WKWebView. The website from Sam Wize may become unavailable in the future and then it is nice to have a self-contained explanation of the solution right here. And again: thanks :-) – kvaruni May 11 '20 at 17:40

1 Answers1

2

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)
Sten
  • 3,624
  • 1
  • 27
  • 26
  • Thanks for this workaround, although my heavy use of tables means that it doesn't really work. Fingers crossed they fix this with iOS 13.4. My current workaround is to export it as an HTML (ie. don't render it as a PDF at all) but this means I can't add my images/graphs. – Paul Martin Feb 12 '20 at 02:47
  • Found a solution (see edit above). It's WAY more elegant and works with macCatalyst and produces PDFs from HTML, with images! – Paul Martin May 11 '20 at 11:09