Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIMarkupTextPrintFormatter and Mac Catalyst

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)
            }
        })
like image 757
Paul Martin Avatar asked Nov 20 '19 03:11

Paul Martin


People also ask

What is Apple catalyst?

Mac Catalyst is the technology that lets you bring your existing iOS applications to macOS, allowing them to take full advantage of the Mac's larger display, integrated keyboard, and mouse or trackpad.

How do I enable catalyst on Mac?

In the target settings, click General to view the general settings of the target. In the Deployment Info section, add Mac as a supported device and enable Mac support. Select the Mac checkbox. Xcode displays an “Enable Mac support?” confirmation after you select the Mac checkbox.


1 Answers

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)
like image 108
Sten Avatar answered Sep 22 '22 03:09

Sten