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)
}
})