Search code examples
swiftcocoansslider

NSSlider custom subclass - how to maintain the link between the knob position and user interaction?


Trying to create a custom NSSlider. Overriding the drawKnob() method of the NSSliderCell changes the knob's appearance but doing this somehow disconnects the link between the knob's position and user interactions with the slider.

In the objective C example that is often referenced (https://github.com/lucasderraugh/LADSlider) it looks like when you override drawKnob() you then need to explicitly deal with the startTracking method, but I haven't found a solution that works for me - at the moment I am just setting the cell's startTracking 'at' property to the current value of the slider, but not sure what the right approach is.

Quite a few examples include an NSCoder argument in the cell's custom initialiser - I don't understand why, but maybe this has something to do with ensuring a connection between the display of the knob and the actual slider value?

import Cocoa

class ViewController: NSViewController {

    var seekSlider = NSSlider()
    var seekSliderCell = SeekSliderCell()

    override func viewDidLoad() {
        
        super.viewDidLoad()
        seekSlider = NSSlider(target: self, action: #selector(self.seek(_:)))
        seekSlider.cell = seekSliderCell
        seekSlider.cell?.target = self
        seekSlider.cell?.action = #selector(self.seek(_:))
        seekSlider.isEnabled = true
        seekSlider.isContinuous = true
        view.addSubview(seekSlider)
    }
    
    @objc func seek(_ sender: NSObject) {
        
        let val = seekSlider.cell?.floatValue
        let point = NSPoint(x: Double(val!), y: 0.0)
        seekSlider.cell?.startTracking(at: point, in: self.seekSlider)
    }
}

class SeekSliderCell: NSSliderCell {
    
//  required init(coder aDecoder: NSCoder) {
//      super.init(coder: aDecoder)
//  }
    
    override func drawKnob() {
        
        let frame = NSRect(x: 0.0, y: 6.0, width: 20.0, height: 10.0)
        let c = NSColor(red: 0.9, green: 0.0, blue: 0.6, alpha: 1.0)
        c.setFill()
        NSBezierPath.init(roundedRect: frame, xRadius: 3, yRadius: 3).fill()
    }

    override func startTracking(at startPoint: NSPoint, in controlView: NSView) -> Bool {

       return true
    }
}



Solution

  • The documentation of drawKnob() states:

    Special Considerations If you create a subclass of NSSliderCell, don’t override this method. Override drawKnob(_:) instead.

    Instead of

    func drawKnob()
    

    override

    func drawKnob(_ knobRect: NSRect)
    

    Example:

    class ViewController: NSViewController {
    
        var seekSlider = NSSlider()
        var seekSliderCell = SeekSliderCell()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            seekSlider = NSSlider(target: self, action: #selector(self.seek(_:)))
            seekSlider.cell = seekSliderCell
            seekSlider.cell?.target = self
            seekSlider.cell?.action = #selector(self.seek(_:))
            seekSlider.isEnabled = true
            seekSlider.isContinuous = true
            view.addSubview(seekSlider)
        }
    
        @objc func seek(_ sender: NSObject) {
            let val = seekSlider.cell?.floatValue
            print("\(String(describing: val))")
        }
    
    }
    
    class SeekSliderCell: NSSliderCell {
    
        override func drawKnob(_ knobRect: NSRect) {
            var frame = NSRect(x: 0.0, y: 6.0, width: 20.0, height: 10.0)
            frame.origin.x = knobRect.origin.x + (knobRect.size.width - frame.size.width) / 2
            frame.origin.y = knobRect.origin.y + (knobRect.size.height - frame.size.height) / 2
            let c = NSColor(red: 0.9, green: 0.0, blue: 0.6, alpha: 1.0)
            c.setFill()
            NSBezierPath.init(roundedRect: frame, xRadius: 3, yRadius: 3).fill()
        }
    
    }