IBDesignable custom NSControl class - frame not resizing in Interface Builder

115 views Asked by At

This is for macOS, not iOS ... thank you.

I have a very simple custom slider control that has 2 @IBInspectable properties - trackHeight and knobHeight. I want my control's frame in Interface Builder to resize in response to those 2 properties changing, so that users get visual feedback during design-time. Now, this works fine during runtime, but NOT in Interface Builder.

What happens in Interface Builder is: the frame stays the same size, but the track and knob (which are custom-drawn by me) of the slider are stretched. This is not what I want. The frame of the control should change.

I have searched a LOT for the solution, and this is what I have in my code, with no luck. Please help. Thanks in advance.

import Cocoa

@IBDesignable
class MySliderControl: NSControl {
    
    override init(frame frameRect: NSRect) {

        super.init(frame: frameRect)
        self.frame = NSMakeRect(frameRect.minX, frameRect.minY, 200, max(trackHeight, knobHeight))
    }
    
    required init?(coder: NSCoder) {
        
        super.init(coder: coder)
        self.frame = NSMakeRect(frame.minX, frame.minY, 200, max(trackHeight, knobHeight))
    }
    
    @IBInspectable var trackHeight: CGFloat = 4 {
        
        didSet {
            
            prepareForInterfaceBuilder()
            needsDisplay = true
        }
    }

    @IBInspectable var knobHeight: CGFloat = 10 {
        
        didSet {
            
            prepareForInterfaceBuilder()
            needsDisplay = true
        }
    }
    
    override func layout() {

        self.setFrameSize(intrinsicContentSize)
        super.layout()
    }
    
    override func prepareForInterfaceBuilder() {
        
        // Resize the frame as a function of trackHeight and knobHeight.

        self.setFrameSize(intrinsicContentSize)
        super.prepareForInterfaceBuilder()
    }
    
    // Width is constant (200), and height is the maximum of knobHeight and trackHeight.
    override var intrinsicContentSize: NSSize {
        NSMakeSize(200, max(trackHeight, knobHeight))
    }
    
    override func draw(_ dirtyRect: NSRect) {
        
        // Center knobRect with respect to barRect.
        let barRect = NSMakeRect(0, knobHeight / 2 - trackHeight / 2, 200, trackHeight)
        let knobRect = NSMakeRect(50, 0, 20, knobHeight)
        
        var path = NSBezierPath(rect: barRect)
        NSColor.gray.setFill()
        path.fill()
        
        path = NSBezierPath(rect: knobRect)
        NSColor.white.setFill()
        path.fill()
    }
}
1

There are 1 answers

1
DonMag On

When overriding intrinsicContentSize, we need to invalidate it to inform IB that it needs to be updated. This will also trigger a call to draw(_ dirtyRect: NSRect).

Give this a try:

@IBDesignable
class MySliderControl: NSControl {
    
    @IBInspectable var trackHeight: CGFloat = 4 {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }
    
    @IBInspectable var knobHeight: CGFloat = 10 {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }
    
    // Width is constant (200), and height is the maximum of knobHeight and trackHeight.
    override var intrinsicContentSize: NSSize {
        NSMakeSize(200, max(trackHeight, knobHeight))
    }
    
    override func draw(_ dirtyRect: NSRect) {
        
        // Vertically Center barRect and knobRect
        //  with respect to self.bounds
        let barRect = NSMakeRect(0, bounds.midY - trackHeight * 0.5, 200, trackHeight)
        let knobRect = NSMakeRect(50, bounds.midY - knobHeight * 0.5, 20, knobHeight)
        
        var path = NSBezierPath(rect: barRect)
        NSColor.gray.setFill()
        path.fill()
        
        path = NSBezierPath(rect: knobRect)
        NSColor.white.setFill()
        path.fill()
        
    }
    
    // unless we're adding some other code,
    //  none of these is needed
    
//  override init(frame frameRect: NSRect) {
//      super.init(frame: frameRect)
//  }
//  required init?(coder: NSCoder) {
//      super.init(coder: coder)
//  }
//  override func layout() {
//      super.layout()
//  }
//  override func prepareForInterfaceBuilder() {
//      super.prepareForInterfaceBuilder()
//  }

}

Edit -- whoops, just saw the date on the post. Hopefully, you already got this worked out :)