23

What is the process of drawing to NSView using storyboards for osx? I have added a NSView to the NSViewController. Then, I added a few constraints and an outlet. enter image description here

Next, I added some code to change the color: import Cocoa

class ViewController: NSViewController {

    @IBOutlet var box: NSView!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewWillAppear() {
        box.layer?.backgroundColor = NSColor.blueColor().CGColor
        //box.layer?.setNeedsDisplay()
    }

    override var representedObject: AnyObject? {
        didSet {
        // Update the view, if already loaded.
        }
    } 
}

I would like to do custom drawing and changing colors of the NSView. I have performed sophisticated drawing on iOS in the past, but am totally stuck here. Does anyone see what I'm doing wrong?

Marek H
  • 5,173
  • 3
  • 31
  • 42
slcott
  • 1,194
  • 3
  • 14
  • 26

10 Answers10

38

The correct way is

class ViewController: NSViewController {

@IBOutlet var box: NSView!

override func viewDidLoad() {
    super.viewDidLoad()
    self.view.wantsLayer = true

}

override func viewWillAppear() {
    box.layer?.backgroundColor = NSColor.blue.cgColor
    //box.layer?.setNeedsDisplay()
}

override var representedObject: AnyObject? {
    didSet {
    // Update the view, if already loaded.
    }
}}
Matt Hampel
  • 5,088
  • 12
  • 52
  • 78
Kon
  • 525
  • 5
  • 11
33

Swift via property

extension NSView {

    var backgroundColor: NSColor? {
        get {
            if let colorRef = self.layer?.backgroundColor {
                return NSColor(CGColor: colorRef)
            } else {
                return nil
            }
        }
        set {
            self.wantsLayer = true
            self.layer?.backgroundColor = newValue?.CGColor
        }
    }   
}

Usage:

    yourView.backgroundColor = NSColor.greenColor()

Where yourView is NSView or any of its subclasses

Updated for Swift 3

extension NSView {

    var backgroundColor: NSColor? {

        get {
            if let colorRef = self.layer?.backgroundColor {
                return NSColor(cgColor: colorRef)
            } else {
                return nil
            }
        }

        set {
            self.wantsLayer = true
            self.layer?.backgroundColor = newValue?.cgColor
        }
    }
}
nsinvocation
  • 7,559
  • 3
  • 41
  • 46
CryingHippo
  • 5,026
  • 1
  • 28
  • 32
16

edit/update:

Another option is to design your own colored view:

import Cocoa
@IBDesignable class ColoredView: NSView {
    @IBInspectable var backgroundColor: NSColor = .clear
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        backgroundColor.set()
        dirtyRect.fill()
    }
}

Then you just need to add a Custom View NSView and set the custom class in the inspector:

Custom View

ColoredView

IBInspectable


Original Answer

Swift 3.0 or later

extension NSView {
    var backgroundColor: NSColor? {
        get {
            guard let color = layer?.backgroundColor else { return nil }
            return NSColor(cgColor: color)
        }
        set {
            wantsLayer = true
            layer?.backgroundColor = newValue?.cgColor
        }
    }
}

let myView = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: 100))
myView.backgroundColor = .red
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • 3
    This is brilliant. I can't believe I didn't think of this. With this simple extension, view backgrounds in Cocoa are now as easy as they are in Cocoa Touch . :) – Clifton Labrum Dec 29 '16 at 04:40
  • The only thing I'd be careful of is overriding existing `backgroundColor` implementations, like on a textview – iwasrobbed May 12 '18 at 14:42
  • On launch, this solution works and custom background color is set. But the color doesn't get applied to the view in interface builder, like in the screenshots (macos 10.15.6), so the bad previews are kinda killing it for me. – Starwave Feb 02 '21 at 17:42
  • @Starwave can't reproduce your issue. it works for me when I change the color using the interface builder 10.15.5. I am using the NSView subclass approach ColoredView. Are you sure there is a backGround user defined attribute key path color set ? https://www.dropbox.com/s/dzgr9ickj0tbn51/user%20defined%20runtime%20attribute.png?dl=1 – Leo Dabus Feb 02 '21 at 17:54
  • 1
    Ok, you are correct, it does work. Opening IB, whole project suddenly started to do a full rebuild, and above the background selection it now says "Designables: Up to date". Means this was some weird IB caching bug. – Starwave Feb 02 '21 at 18:03
  • I like this... works for me though it did generate some random odd errors require some fiddling and ultimately a restart of xCode – Jc Nolan May 06 '22 at 17:11
5

This works a lot better:

    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)
        NSColor.blueColor().setFill()
        NSRectFill(dirtyRect);
    }
slcott
  • 1,194
  • 3
  • 14
  • 26
  • define: "better". Than what? drawRect has performance implications that the NSVIew extension does not. – verec May 28 '16 at 11:59
4

Best way to set a NSView background colour in MacOS 10.14 with dark mode support :

1/ Create your colour in Assets.xcassets enter image description here

2/ Subclass your NSView and add this :

class myView: NSView {
    override func updateLayer() {
       self.layer?.backgroundColor = NSColor(named: "customControlColor")?.cgColor
    }
}

Very simple and dark mode supported with the colour of your choice !

Full guide : https://developer.apple.com/documentation/appkit/supporting_dark_mode_in_your_interface

user3722523
  • 1,740
  • 2
  • 15
  • 27
  • 1
    I think this is the best approach – hcarrasko Jun 07 '19 at 18:28
  • Indeed, this is the correct approach today, due to Dark Mode. Unless your target color is acceptable for both Light and Dark modes, the earlier accepted answer will not update the background color if the user changes their Light vs. Dark System Preference while the view is being displayed. This answer will. – Jerry Krinock Jul 26 '19 at 18:01
  • And, if you want an appropriate light or dark gray, you don't need to create an asset. Use one of the "chameleon" NSColor objects which switches automatically for Light vs. Dark mode, such as `NSColor.controlBackgroundColor`. – Jerry Krinock Jul 26 '19 at 18:19
  • This is correct, I'd add that your view also must override wantsUpdateLayer : Bool like: ` override var wantsUpdateLayer : Bool { get { return true } }` – Jackifus Jan 23 '22 at 02:44
3

Just one line of code is enough to change the background of any NSView object:

myView.setValue(NSColor.blue, forKey: "backgroundColor")

Instead of this, you can also add an user defined attribute in the interface designer of type Color with keyPath backgroundColor.

Ely
  • 8,259
  • 1
  • 54
  • 67
1

Update to Swift 3 solution by @CryingHippo (It showed colors not on every run in my case). I've added DispatchQueue.main.async and now it shows colors on every run of the app.

extension NSView {

    var backgroundColor: NSColor? {

        get {
            if let colorRef = self.layer?.backgroundColor {
                return NSColor(cgColor: colorRef)
            } else {
                return nil
            }
        }

        set {

            DispatchQueue.main.async { 

                self.wantsLayer = true
                self.layer?.backgroundColor = newValue?.cgColor

            }

        }
    }
}
Adelmaer
  • 2,209
  • 3
  • 22
  • 45
1

None of the solutions is using pure power of Cocoa framework. The correct solution is to use NSBox instead of NSView. It has always supported fillColor, borderColor etc.

  1. Set titlePosition to None
  2. Set boxType to Custom
  3. Set borderType to None
  4. Set your desired fillColor from asset catalogue (IMPORTANT for dark mode)
  5. Set borderWidth and borderRadius to 0

Bonus:

  • it dynamically reacts to sudden change of appearance (light to dark)
  • it supports animations ( no need for dynamic + no need to override animation(forKey key:NSAnimatablePropertyKey) ->)
  • future macOS support automatically

WARNING:

  • Using NSBox + system colors in dark mode will apply tinting corectly. If you do not want tinting use catalogue color.

enter image description here

Alternative is to provide subclass of NSView and do the drawing updates in updateLayer

import Cocoa

@IBDesignable
class CustomView: NSView {

    @IBInspectable var backgroundColor : NSColor? {
        didSet { needsDisplay = true }
    }
    override var wantsUpdateLayer: Bool {
        return true
    }

    override func updateLayer() {
        guard let layer = layer else { return }
        layer.backgroundColor = backgroundColor?.cgColor
    }
}

Tinting: enter image description here

Marek H
  • 5,173
  • 3
  • 31
  • 42
0

Since macOS 10.13 (High Sierra) NSView responds to selector backgroundColor although it is not documented!

Frank Hintsch
  • 560
  • 8
  • 14
0

There is a drawback when using a backgroundColor property of NSView's layer. If appearance did change, cgColor do not change. According to a suggestion, use NSBox's fillColor property. For example subclass it.

import AppKit

class BasicView: NSBox {
    
    // MARK: - Properties
    
    var backgroundColor: NSColor {
        get { fillColor }
        set { fillColor = newValue }
    }
    
    // MARK: - Lifecycle
    
    init() {
        super.init(frame: .zero)
        configureView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Methods
    
    private func configureView() {
        boxType = .custom
        contentViewMargins = .zero
        borderWidth = 0
        borderColor = .clear
    }
}

But I think it is much better to use CALayer backed NSView:

import AppKit

final class BasicView: NSView {
    
    // MARK: - Properties
    
    var backgroundColor: NSColor? {
        didSet { configureBackground() }
    }
    
    override var wantsUpdateLayer: Bool {
        get { true }
    }
    
    // MARK: - Lifecycle
    
    init() {
        super.init(frame: .zero)
        configureView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Methods
    
    override func updateLayer() {
        super.updateLayer()
        configureBackground()
    }
    
    private func configureView() {
        wantsLayer = true
        layerContentsRedrawPolicy = .onSetNeedsDisplay
    }
    
    private func configureBackground() {
        guard let layer else {
            return
        }
        
        if let backgroundColor {
            layer.backgroundColor = backgroundColor.cgColor
        } else {
            layer.backgroundColor = nil
        }
    }
}
Ace Rodstin
  • 132
  • 1
  • 6