Setting backgroundColor of custom NSView
Asked Answered
A

10

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?

Alvera answered 11/1, 2015 at 17:46 Comment(0)
A
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.
    }
}}
Annice answered 24/5, 2015 at 16:1 Comment(4)
Is there an advantage in using a layer backed view?Alvera
What's the purpose of wantsLayer?Cornejo
developer.apple.com/library/mac/documentation/Cocoa/Reference/…Annice
There is no need to use wantsLayer if you are not changing its background color.Frauenfeld
V
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
        }
    }
}
Vigorous answered 16/7, 2015 at 18:0 Comment(0)
F
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
Frauenfeld answered 3/7, 2015 at 21:31 Comment(6)
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 . :)Carmel
The only thing I'd be careful of is overriding existing backgroundColor implementations, like on a textviewVestavestal
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.Ferdinande
@Ferdinande 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 ? dropbox.com/s/dzgr9ickj0tbn51/…Frauenfeld
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.Ferdinande
I like this... works for me though it did generate some random odd errors require some fiddling and ultimately a restart of xCodePhippen
A
5

This works a lot better:

    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)
        NSColor.blueColor().setFill()
        NSRectFill(dirtyRect);
    }
Alvera answered 11/1, 2015 at 20:59 Comment(1)
define: "better". Than what? drawRect has performance implications that the NSVIew extension does not.Bemuse
K
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

Katakana answered 9/12, 2018 at 16:33 Comment(4)
I think this is the best approachOctastyle
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.Tatman
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.Tatman
This is correct, I'd add that your view also must override wantsUpdateLayer : Bool like: ` override var wantsUpdateLayer : Bool { get { return true } }`Despicable
S
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.

Silberman answered 9/4, 2020 at 9:15 Comment(1)
I tried everything and this is the only thing that works for meDisquisition
S
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

            }

        }
    }
}
Senn answered 7/9, 2017 at 11:0 Comment(0)
M
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

Moeller answered 9/4, 2020 at 8:57 Comment(0)
A
0

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

Artemus answered 7/12, 2017 at 10:27 Comment(0)
D
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
        }
    }
}
Dezhnev answered 26/7, 2023 at 7:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.