Search code examples
layernsviewnspasteboard

How do I get a layer backed NSView to write to PDF in the clipboard


I am using a layer backed view with a CGImage and would like to use the NSView.writePDF(inside:to) API to copy the views image to the clipboard. The view has subviews that I would like included in the PDF.

I have created a Xcode playground to illustrate the problem.

//: A Cocoa based Playground to test NSView.writePDF()

import AppKit
import PlaygroundSupport

class BaseView: NSView {

    var isSelected: Bool = false {
        didSet {
            self.needsDisplay = true
        }
    }

    var fillColor = NSColor.yellow

    var cgFillColor: CGColor {
        return self.isSelected ? fillColor.cgColor : fillColor.withAlphaComponent(0.5).cgColor
    }

    var storeLayoutCGImageRef: CGImage? {
        didSet {

            self.layer?.contents = storeLayoutCGImageRef

        }
    }

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        guard let context = NSGraphicsContext.current?.cgContext else {
            return
        }

        context.setFillColor(cgFillColor)
        context.fill(dirtyRect)
    }

    override func mouseDown(with theEvent: NSEvent) {

        self.isSelected = !self.isSelected
    }

    @objc func copyImage(_ sender: Any){
         let pb = NSPasteboard.general
        pb.declareTypes([.pdf], owner: self)

        self.writePDF(inside: self.bounds, to: pb)
    }
    @objc func doNothing(_ sender: Any){

    }
    // Load image in the background
    func loadImage(completion: @escaping ()->Void){
        DispatchQueue.global().async {
            if let image = NSImage(named: "B-019969.jpg") {

                // Lets use double the size ?
                var rect = CGRect(x: 0, y: 0, width: image.size.width*3.0, height: image.size.height*3.0)
                let cgImage = image.cgImage(forProposedRect: &rect, context: nil, hints: nil)

                DispatchQueue.main.async {
                    self.storeLayoutCGImageRef = cgImage

                    completion()
                }
                return
            }

            DispatchQueue.main.async {
                //self.storeLayoutImage = nil
                self.storeLayoutCGImageRef = nil
                completion()
            }
        }
    }
}

class ModuleView: BaseView {

    let color = NSColor.purple

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        self.fillColor = color
    }

    required init?(coder decoder: NSCoder) {
         super.init(coder: decoder)
        self.fillColor = color
    }

    override func menu(for event: NSEvent) -> NSMenu? {
        if !isSelected {
            let menu = NSMenu()
            menu.addItem(NSMenuItem(title: "Copy module", action: #selector(copyImage(_:)), keyEquivalent: ""))
            return menu
        } else {
            // Display popup menu
            let menu = NSMenu()
            menu.addItem(NSMenuItem(title: "Copy module", action: #selector(copyImage(_:)), keyEquivalent: ""))
            menu.addItem(NSMenuItem(title: "Do Something", action: #selector(doNothing(_:)), keyEquivalent: ""))
            return menu

        }
    }

}
class ShelfView: BaseView {

    let color = NSColor.yellow

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        self.fillColor = color
        self.loadImage {
            print("Image loaded")
        }
    }
    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        self.fillColor = color
    }
    override func menu(for event: NSEvent) -> NSMenu? {
        if !isSelected {
            let menu = NSMenu()
            menu.addItem(NSMenuItem(title: "Copy shelf", action: #selector(copyImage(_:)), keyEquivalent: ""))
            return menu
        } else {
            // Display popup menu
            let menu = NSMenu()
            menu.addItem(NSMenuItem(title: "Copy shelf", action: #selector(copyImage(_:)), keyEquivalent: ""))
            menu.addItem(NSMenuItem(title: "Do Something", action: #selector(doNothing(_:)), keyEquivalent: ""))
            return menu

        }
    }
    override var wantsUpdateLayer: Bool {
        return true
    }
    override func updateLayer() {
        self.layer?.backgroundColor = self.cgFillColor
        self.layer?.contents = self.storeLayoutCGImageRef
    }
}

class view: BaseView
{

    var module: ModuleView?

    let color = NSColor.cyan

    override init(frame: NSRect)
    {
        super.init(frame: frame)
        self.fillColor = color
        self.addModules()
        self.addShelves()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func addModules(){

        let module = ModuleView()

        module.frame = NSRect(x: 10.0, y: 10.0, width: 180.0, height: 180.0)
        self.module = module
        self.addSubview(module)
    }
    func addShelves(){

        let shelf = ShelfView()

        shelf.frame = NSRect(x: 10.0, y: 10.0, width:160.0, height: 160.0)
        self.module?.addSubview(shelf)
    }

    override func menu(for event: NSEvent) -> NSMenu? {
        if !isSelected {
            let menu = NSMenu()
            menu.addItem(NSMenuItem(title: "Copy fixture", action: #selector(copyImage(_:)), keyEquivalent: ""))
            return menu
        } else {
            // Display popup menu
            let menu = NSMenu()
            menu.addItem(NSMenuItem(title: "Copy fixture", action: #selector(copyImage(_:)), keyEquivalent: ""))
            menu.addItem(NSMenuItem(title: "Do Something", action: #selector(doNothing(_:)), keyEquivalent: ""))
            return menu

        }
    }
}

var v = view(frame: NSRect(x: 0, y: 0, width: 200, height: 200))

PlaygroundPage.current.liveView = v

Is there a way to make sure the layer gets included in the PDF output?


Solution

  • OK, I just figured it out - basically you have to implement the draw(_ dirtyRect) function which gets called during printing or when the NSView.writePDF() is called.

    Note that the draw() function won't get called under certain conditions - e.g. selecting the item with the mouse will only result in the updateLayer() method being called.

    Here is the updated playground, hope this saves someone else some time.

        //: A Cocoa based Playground to test NSView.writePDF()
        
        import AppKit
        import PlaygroundSupport
        
        class BaseView: NSView {
            
            var isSelected: Bool = false {
                didSet {
                    self.needsDisplay = true
                }
            }
            
            var fillColor = NSColor.yellow
            
            var cgFillColor: CGColor {
                return self.isSelected ? fillColor.cgColor : fillColor.withAlphaComponent(0.5).cgColor
            }
            
            var storeLayoutCGImageRef: CGImage? {
                didSet {
                    
                    self.layer?.contents = storeLayoutCGImageRef
                    
                }
            }
            
            var image: CGImage?
            
            override func draw(_ dirtyRect: NSRect) {
                super.draw(dirtyRect)
                guard let context = NSGraphicsContext.current?.cgContext else {
                    return
                }
                // NB THIS PART !!
                self.layer?.render(in:context)
            }
            
            override func mouseDown(with theEvent: NSEvent) {
                
                self.isSelected = !self.isSelected
            }
            
            @objc func copyImage(_ sender: Any){
                 let pb = NSPasteboard.general
                pb.declareTypes([.pdf], owner: self)
                
                self.writePDF(inside: self.bounds, to: pb)
            }
            @objc func doNothing(_ sender: Any){
                
            }
            // Load image in the background
            func loadImage(completion: @escaping ()->Void){
                DispatchQueue.global().async {
                    if let image = NSImage(named: "Sample Image.png") {
                        
                        // Lets use double the size ?
                        var rect = CGRect(x: 0, y: 0, width: image.size.width*3.0, height: image.size.height*3.0)
                        let cgImage = image.cgImage(forProposedRect: &rect, context: nil, hints: nil)
                        
                        DispatchQueue.main.async {
                            self.storeLayoutCGImageRef = cgImage
                            
                            completion()
                        }
                        return
                    }
                    
                    DispatchQueue.main.async {
                        //self.storeLayoutImage = nil
                        self.storeLayoutCGImageRef = nil
                        completion()
                    }
                }
            }
        }
        
        class ModuleView: BaseView {
            
            let color = NSColor.purple
            
            override init(frame frameRect: NSRect) {
                super.init(frame: frameRect)
                self.fillColor = color
            }
            
            required init?(coder decoder: NSCoder) {
                 super.init(coder: decoder)
                self.fillColor = color
            }
            
            override func menu(for event: NSEvent) -> NSMenu? {
                if !isSelected {
                    let menu = NSMenu()
                    menu.addItem(NSMenuItem(title: "Copy module", action: #selector(copyImage(_:)), keyEquivalent: ""))
                    return menu
                } else {
                    // Display popup menu
                    let menu = NSMenu()
                    menu.addItem(NSMenuItem(title: "Copy module", action: #selector(copyImage(_:)), keyEquivalent: ""))
                    menu.addItem(NSMenuItem(title: "Do Something", action: #selector(doNothing(_:)), keyEquivalent: ""))
                    return menu
                    
                }
            }
            
        }
        class ShelfView: BaseView {
            
            let color = NSColor.green
            
            override init(frame frameRect: NSRect) {
                super.init(frame: frameRect)
                self.fillColor = color
                self.loadImage {
                    print("Image loaded")
                }
            }
            required init?(coder decoder: NSCoder) {
                super.init(coder: decoder)
                self.fillColor = color
            }
            override func menu(for event: NSEvent) -> NSMenu? {
                if !isSelected {
                    let menu = NSMenu()
                    menu.addItem(NSMenuItem(title: "Copy shelf", action: #selector(copyImage(_:)), keyEquivalent: ""))
                    return menu
                } else {
                    // Display popup menu
                    let menu = NSMenu()
                    menu.addItem(NSMenuItem(title: "Copy shelf", action: #selector(copyImage(_:)), keyEquivalent: ""))
                    menu.addItem(NSMenuItem(title: "Do Something", action: #selector(doNothing(_:)), keyEquivalent: ""))
                    return menu
                    
                }
            }
            
            override func draw(_ dirtyRect: NSRect) {
                print("ShelfView.draw() called")
                super.draw(dirtyRect)
                guard let context = NSGraphicsContext.current?.cgContext else {
                    return
                }
                if let image = self.storeLayoutCGImageRef {
                    context.draw(image, in: self.bounds)
                }
            }
            
            override var wantsUpdateLayer: Bool {
                return true
            }
            override func updateLayer() {
                print("updateLayer() called")
                self.layer?.backgroundColor = self.cgFillColor
                self.layer?.contents = self.storeLayoutCGImageRef
                
            }
        }
        
        class view: BaseView
        {
            
            var module: ModuleView?
            
            let color = NSColor.cyan
            
            override init(frame: NSRect)
            {
                super.init(frame: frame)
                self.fillColor = color
                self.addModules()
                self.addShelves()
            }
            
            required init?(coder: NSCoder) {
                fatalError("init(coder:) has not been implemented")
            }
            
            func addModules(){
                
                let module = ModuleView()
                
                module.frame = NSRect(x: 10.0, y: 10.0, width: 180.0, height: 180.0)
                self.module = module
                self.addSubview(module)
            }
            func addShelves(){
                
                let shelf = ShelfView()
                
                shelf.frame = NSRect(x: 10.0, y: 10.0, width:160.0, height: 160.0)
                self.module?.addSubview(shelf)
            }
            
            override func menu(for event: NSEvent) -> NSMenu? {
                if !isSelected {
                    let menu = NSMenu()
                    menu.addItem(NSMenuItem(title: "Copy fixture", action: #selector(copyImage(_:)), keyEquivalent: ""))
                    return menu
                } else {
                    // Display popup menu
                    let menu = NSMenu()
                    menu.addItem(NSMenuItem(title: "Copy fixture", action: #selector(copyImage(_:)), keyEquivalent: ""))
                    menu.addItem(NSMenuItem(title: "Do Something", action: #selector(doNothing(_:)), keyEquivalent: ""))
                    return menu
                    
                }
            }
        }
        
        var v = view(frame: NSRect(x: 0, y: 0, width: 200, height: 200))
        
        PlaygroundPage.current.liveView = v