Printing without tears in Dark Mode (and exporting to PDF)

Early in January, I detailed here two issues with printing in apps developed using the main Cocoa frameworks which make up AppKit: the first is that printing in Dark Mode propagates that appearance to the pages it outputs, and the second is that print margins are set incorrectly and crop lines.

The first problem results from a behaviour which creates work for the developer, that should surely have been handled by AppKit. Its existence is hinted at in the AppKit release notes for Mojave:
“When you print an NSView through an NSPrintOperation, its appearance now gets temporarily replaced by the aqua appearance during rendering. This is done to avoid printing with an inherited dark appearance.”
That sounds very reassuring until you read:
“To avoid altering the contents of on-screen windows, the darkAqua appearance isn’t replaced when printing views that are simultaneously hosted in a window.”
So when you print a window which the user can see, AppKit lets it get printed in Dark Mode if that is in use. That’s not at all helpful, is it?

The answer is to print from a separate off-screen view, which you can set to Light Mode. I had been looking for a solution to do this ever since, and this week stumbled across a Swift AppKit example project tucked away in the latest developer documentation, which demonstrates this neatly. I have now applied this at both Document and View level to my apps DelightEd and Podofyllin. I’ll show my example from the code for Podofyllin, which is Document-based.

To handle printing in Dark Mode properly, you need to add the following three functions to your custom Document.

The first returns the PrintInfo which will set margins to avoid cropping of lines:
func thePrintInfo() -> NSPrintInfo {
let thePrintInfo = NSPrintInfo()
thePrintInfo.horizontalPagination = .fit
thePrintInfo.verticalPagination = .automatic
thePrintInfo.isHorizontallyCentered = false
thePrintInfo.isVerticallyCentered = false
thePrintInfo.leftMargin = 72.0
thePrintInfo.rightMargin = 72.0
thePrintInfo.topMargin = 72.0
thePrintInfo.bottomMargin = 72.0
thePrintInfo.jobDisposition = .spool
printInfo.dictionary().setObject(NSNumber(value: true), forKey: NSPrintInfo.AttributeKey.headerAndFooter as NSCopying)
return thePrintInfo

The second is an empty Objective-C function to handle the result of your new PrintOperation:
@objc func printOperationDidRun(
_ printOperation: NSPrintOperation, success: Bool, contextInfo: UnsafeMutableRawPointer?) {

The third is where all the real work is done, in a printDocument action. This starts by setting up page sizes and margins to avoid line cropping, then creates the new view, here an NSTextView, for off-screen use:
@IBAction override func printDocument(_ sender: Any?) {
let pageSize = NSSize(width: (printInfo.paperSize.width), height: (printInfo.paperSize.height))
let textView = NSTextView(frame: NSRect(x: 0.0, y: 0.0, width: pageSize.width - 144.0, height: pageSize.height))

Now comes setting the Light Mode appearance, and copying the attributed text from the on-screen view into the off-screen one:
textView.appearance = NSAppearance(named: .aqua)
if let textView2 = viewController?.textView {
if let textView3 = textView2.textStorage {

With the off-screen view all ready, a new PrintOperation is created for that using the PrintInfo, and run modally, calling printOperationDidRun on completion:
let printOperation = NSPrintOperation(view: textView, printInfo: self.thePrintInfo())
printOperation.runModal(for: windowControllers[0].window!, delegate: self, didRun: #selector(printOperationDidRun(_:success:contextInfo:)), contextInfo: nil)


To print a document, you then need to set the printDocument action for your Print menu command, in Interface Builder.

You can adapt this to operate at a View level by overriding that view’s printView() when necessary, or for PDF views in a PDFViewDelegate as a pdfViewPerformPrint() function, which is how Podofyllin handles this.

Whilst we’re here, another useful trick is to be able to export PDF files directly without forcing the user to go through the Print dialog. In DelightEd, I now do this directly using a similar off-screen view.

This is set in a simple action function called from a menu command, and first goes through the same steps to draw the NSTextView into an off-screen view held in Light Mode:
@IBAction func exportPDF(_ sender: Any?) {
if let textView = viewController?.textView {
let textView2 = NSTextView(frame: textView.frame)
textView2.appearance = NSAppearance(named: .aqua)
if let textView3 = textView.textStorage {

A custom PrintInfo is then set up, with the jobDisposition set to save the output as PDF:
let thePrintInfo1 = self.printInfo
thePrintInfo1.horizontalPagination = .fit
thePrintInfo1.verticalPagination = .automatic
thePrintInfo1.isHorizontallyCentered = false
thePrintInfo1.isVerticallyCentered = false
thePrintInfo1.leftMargin = 72.0
thePrintInfo1.rightMargin = 72.0
thePrintInfo1.topMargin = 72.0
thePrintInfo1.bottomMargin = 72.0
thePrintInfo1.jobDisposition = .save

This is set up as a PrintOperation which doesn’t display the Print dialog, but does show a progress window, to cater for the writing of longer PDF documents:
let thePrintOp = NSPrintOperation.init(view: textView2, printInfo: thePrintInfo1)
thePrintOp.showsPrintPanel = false
thePrintOp.showsProgressPanel = true


I hope that these snippets help you address these problems which have been left to trip us up in AppKit. If only Apple did the same in its Calculator and other Mojave tools which still, in Mojave 10.14.4, don’t print properly in Dark Mode. That’s the problem with not implementing this sort of feature in AppKit itself: every single AppKit app has to use near-identical code to override the defective default behaviour.