Search code examples
swiftcocoansoutlineview

How to give an NSOutlineView a tiled background image that scrolls?


I have a standard NSOutlineView. I would like it to have a background image, which tiles vertically, and which scrolls together with the outline view cells.

I've somewhat achieved this using the following in my ViewController:

class ViewController: NSViewController {

    @IBOutlet weak var outlineView: NSOutlineView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        if let image = NSImage(named: "tile") {
            let color = NSColor.init(patternImage: image)
            outlineView.backgroundColor = color
        }
    }
}

That works, except when you scroll past the top or bottom of the view (with the stretch provided by the containing scroll view).

enter image description here

I've tried putting the background image on the scroll view, but then it is static and doesn't scroll with the outline view's content.

I've also tried subclassing various objects in the view hierarchy and overriding their draw(_ dirtyRect: NSRect) method and doing:

self.wantsLayer = true
self.layer?.backgroundColor = ...etc

but got no success from that either.

Can anyone provide any suggestions?


Solution

  • I ended up creating a new custom NSView:

    class MyView: NSView {
    
        override func draw(_ dirtyRect: NSRect) {
            if let image = NSImage(named: "Tile") {
                let color = NSColor.init(patternImage: image)
                color.setFill()
                dirtyRect.fill()
            }
    
            super.draw(dirtyRect)
        }
    }
    

    Then in my ViewController class I added an instance of the custom view, and used autolayout constraints to pin the new view to my outlineView's clip view starting 2000points above it, and ending 2000 below. This means no matter how far you over-scroll into the stretch area, you still see the tiled background.

    class MyViewController: NSViewController {
        @IBOutlet weak var outlineView: NSOutlineView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            guard let clipView = self.outlineView.superview else { return }
    
            let newView = MyView(frame: .zero) // Frame is set by autolayout below.
            newView.translatesAutoresizingMaskIntoConstraints = false
    
            clipView.addSubview(newView, positioned: .below, relativeTo: self.outlineView)
    
            // Add autolayout constraints to pin the new view to the clipView.
            // See https://apple.co/3c6EMcH
            newView.leadingAnchor.constraint(equalTo: clipView.leadingAnchor).isActive = true
            newView.widthAnchor.constraint(equalTo: clipView.widthAnchor).isActive = true
            newView.topAnchor.constraint(equalTo: clipView.topAnchor, constant: -2000).isActive = true
            newView.bottomAnchor.constraint(equalTo: clipView.bottomAnchor, constant: 2000).isActive = true
        }
    }
    

    I've removed other code from the above so hopefully I've left everything needed to illustrate the solution.