9
votes

I'm subclassing NSButtonCell to customize the drawing (customizable theme). I'd like to customize the way checkboxes and radio buttons are drawn.

Does anyone know how to detect whether a button is a checkbox or radio button?

There is only -setButtonType:, no getter, and neither -showsStateBy nor -highlightsBy seem to give any unique return values for checkboxes that don't also apply to regular push buttons with images and alternate images.

So far I've found two (not very pretty) workarounds, but they're the kind of thing that'd probably get the app rejected from MAS:

  1. Use [self valueForKey: @"buttonType"]. This works, but since the method is not in the headers, I presume this is something Apple wouldn't want me to do.

  2. Override -setButtonType: and -initWithCoder: to keep track of the button type when it is set manually or from the XIB. Trouble here is the XIB case, because the keys used to save the button type to disk are undocumented. So again, I'd be using private API.

I'd really like this to be a straight drop-in replacement for NSButtonCell instead of forcing client code to use a separate ULIThemeSwitchButtonCell class for checkboxes and a third one for radio buttons.

4
For what it's worth, [self valueForKey: @"buttonType"] doesn't work on OS 10.7, it throws an undefined key exception.JWWalker

4 Answers

2
votes

A button does not know anything about its style.

From the documentation on NSButton

Note that there is no -buttonType method. The set method sets various button properties that together establish the behavior of the type. -

You could use tag: and setTag: (inherited by NSButton from NSControl) in order to mark the button either as a checkbox or a radio button. If you do that programatically then you should define the constant you use. You can also set the tag in Interface Builder, but only as an integer value (magic number).

0
votes

In initWithCoder, here is my adaptation of the BGHUDButtonCell.m solution, updated for Mac OS Sierra:

-(id)initWithCoder:(NSCoder *)aDecoder {

   if ( !(self = [super initWithCoder: aDecoder]) ) return nil;

   NSImage *normalImage = [aDecoder decodeObjectForKey:@"NSNormalImage"];
   if ( [normalImage isKindOfClass:[NSImage class]] )
   {
      DLog( @"buttonname %@", [normalImage name] );
      if ( [[normalImage name] isEqualToString:@"NSSwitch"] )
         bgButtonType = kBGButtonTypeSwitch;
      else if ( [[normalImage name] isEqualToString:@"NSRadioButton"] )
         bgButtonType = kBGButtonTypeRadio;
   }
   else
   {
      // Mac OS Sierra update (description has word "checkbox")
      NSImage *img = [self image];
      if ( img && [[img description] rangeOfString:@"checkbox"].length )
      {
         bgButtonType = kBGButtonTypeSwitch;
      }
   }
}
0
votes

This is strange to me that it's missing from NSButton. I don't get it. That said, it's easy enough to extend NSButton to store the last set value:

import Cocoa

public class TypedButton: NSButton {
    private var _buttonType: NSButton.ButtonType = .momentaryLight
    public var buttonType: NSButton.ButtonType {
        return _buttonType
    }

    override public func setButtonType(_ type: NSButton.ButtonType) {
        super.setButtonType(type)
        _buttonType = type
    }
}
0
votes

Swift 5.5

This is my approach. I use a standard naming convention in my app that relies on plain language identifiers. All my UI elements incorporate their respective property names and what type of UI element is associated with the property. It can make for some pretty long IBOutlet and IBAction names, but remembering tag numbers is way too complicated for me.

For example:

@IBOutlet weak var serveBeerCheckbox: NSButton!
@IBOutlet weak var headSize0RadioButton: NSButton!
@IBOutlet weak var headSize1RadioButton: NSButton!
@IBOutlet weak var headSize2RadioButton: NSButton!
\\ etc.

If there are UI properties that need to be stored, I name those without the type of UI element:

var serveBeer: Bool = true
var headSize: Int = 1

Bare bones example:

import Cocoa

class ViewController: NSViewController {
    
    @IBOutlet weak var serveBeerCheckbox: NSButton!
    @IBOutlet weak var headSize0RadioButton: NSButton!
    @IBOutlet weak var headSize1RadioButton: NSButton!
    @IBOutlet weak var headSize2RadioButton: NSButton!

    var serveBeer: Bool = true
    var headSize: Int = 1

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    @IBAction func buttonClicked(button: NSButton) {
        guard let identifier = button.identifier else { return }
        if identifier.rawValue.contains("Checkbox") {
            switch button.identifier {
                case serveBeerCheckbox.identifier:
                // Do something with the Checkbox
                serveBeer = (serveBeerCheckbox?.state == .on)
                default:
                    // Another checkbox button
            }
        } else if identifier.rawValue.contains("RadioButton") {
            switch button.identifier {
                case headSize0RadioButton.identifier:
                    headSize = 0
                case headSize1RadioButton.identifier:
                    headSize = 1
                case headSize2RadioButton.identifier:
                    headSize = 2
                default:
            }
        } // You could continue checking for different types of buttons
        print("Serve beer? \(serveBeer ? "Sure!" : "Sorry, no.")")
        if serveBeer {
            switch headSize {
                case 1:
                    print("With one inch of head.")
                case 2:
                    print("With two inches of head!")
                default:
                    print("Sorry, no head with your beer.")
            }
        }
    }
}

As you can see, one could write a very generic method that can work on any type of UI element and use the rawValue of the identifier string with .contains() to isolate the type of element being worked with.

I have found using this approach allows me to initialize a UI with a lot of different elements pretty quickly and efficiently without having to recall tag numbers.