60

In my desktop Mac OS X app, I'd like to programatically create a NSTextField "label" which has the same behavior and properties as a typical label created in Interface Builder.

I usually use (and very much like) IB, but in this case it must be done programatically.

Try as I might, I can't seem to find the combination of method calls that will programatically produce the same label-y behavior as a "Label" dragged from the IB View Library palette.

Can anyone provide or point out some example code of how to do this programatically? Thx.

Todd Ditchendorf
  • 11,217
  • 14
  • 69
  • 123
  • Not clear what the problem is. You can't just create an NSTextField? – Hunter Oct 02 '09 at 03:43
  • sure i can. and i have. but i can't discover the sequence of methods to call on the NSTextField to get it to behave like a "Label" dragged from IB. setting no border, no bezel, not editable, not selectable results in a NSTextField that does not look or behave like the IB "Label". I'm really hoping someone can point me to some existing code that does this. – Todd Ditchendorf Oct 02 '09 at 03:57

6 Answers6

123

A label is actually an instance of NSTextField, a subclass of NSView. So, since it is a NSView, it has to be added to another view.

Here's a working code:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSTextField *textField;

    textField = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 10, 200, 17)];
    [textField setStringValue:@"My Label"];
    [textField setBezeled:NO];
    [textField setDrawsBackground:NO];
    [textField setEditable:NO];
    [textField setSelectable:NO];
    [view addSubview:textField];
}
Guru
  • 21,652
  • 10
  • 63
  • 102
  • 2
    yeah, i'm really looking for more than this basic information. That does not produce the 'label-y' behavior seen in IB. – Todd Ditchendorf Oct 02 '09 at 03:52
  • this is pretty much exactly what i've tried. does not produce label-y behavior like in IB :( – Todd Ditchendorf Oct 02 '09 at 04:03
  • Really? It does work for me though: http://dl.getdropbox.com/u/925956/label-test.png. Or maybe I do not understand what you mean by "label-y"? – Thibault Martin-Lagardette Oct 02 '09 at 04:04
  • 1
    my bad. i was wrong. i had something else weird that was going wrong. this does indeed get me very close to what i'm looking for. :) – Todd Ditchendorf Oct 02 '09 at 04:27
  • 2
    The only thing I see missing there is the font is wrong. If you're completely creating the text field and want to use the default system fonts: [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:[[textField cell] controlSize]]] – Ashley Clark Oct 02 '09 at 04:46
  • Don't forget to set the autoresizing mask. i.e.: `[textField setAutoresizingMask: NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin | NSViewMinYMargin | NSViewHeightSizable | NSViewMaxYMargin];` – nacho4d Apr 21 '11 at 06:01
31

macOS 10.12 and Later

Starting with macOS 10.12 (Sierra), there are three new NSTextField constructors:

  • NSTextField(labelWithString:), which the header file comment says “Creates a non-wrapping, non-editable, non-selectable text field that displays text in the default system font.”

  • NSTextField(wrappingLabelWithString:), which the header file comment says “Creates a wrapping, non-editable, selectable text field that displays text in the default system font.”

  • NSTextField(labelWithAttributedString:), which the header file comment says “Creates a non-editable, non-selectable text field that displays attributed text. The line break mode of this field is determined by the attributed string's NSParagraphStyle attribute.”

I tested the ones that take a plain (non-attributed string), and they create text fields that are similar to, but not precisely the same as, the text fields created in a storyboard or xib.

The important difference is that both constructors create a text field with textBackgroundColor (normally pure white) as its background color, while the storyboard text field uses controlColor (normally about 90% white).

Unimportantly, both constructors also set their fonts by calling NSFont.systemFont(ofSize: 0) (which produces a different NSFont object than my code below, but they wrap the same underlying Core Text font).

The wrappingLabelWithString: constructor sets the field's isSelectable to true. (This is documented in the header file.)


macOS 10.11 and Earlier

I compared four NSTextField instances: one created by dragging a “Label” to a storyboard, another created by dragging a “Wrapping Label” to a storyboard, and two in code. Then I carefully modified properties of the code-created labels until all their properties were exactly the same as the storyboard-created labels. These two methods are the result:

extension NSTextField {

    /// Return an `NSTextField` configured exactly like one created by dragging a “Label” into a storyboard.
    class func newLabel() -> NSTextField {
        let label = NSTextField()
        label.isEditable = false
        label.isSelectable = false
        label.textColor = .labelColor
        label.backgroundColor = .controlColor
        label.drawsBackground = false
        label.isBezeled = false
        label.alignment = .natural
        label.font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: label.controlSize))
        label.lineBreakMode = .byClipping
        label.cell?.isScrollable = true
        label.cell?.wraps = false
        return label
    }

    /// Return an `NSTextField` configured exactly like one created by dragging a “Wrapping Label” into a storyboard.
    class func newWrappingLabel() -> NSTextField {
        let label = newLabel()
        label.lineBreakMode = .byWordWrapping
        label.cell?.isScrollable = false
        label.cell?.wraps = true
        return label
    }

}

If you use one of these methods, don't forget to set your field's frame, or turn off its translatesAutoresizingMaskIntoConstraints and add constraints.


Here is the code I used to compare the different text fields, in case you want to check:

import Cocoa

class ViewController: NSViewController {

    @IBOutlet var label: NSTextField!
    @IBOutlet var multilineLabel: NSTextField!

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

    override func viewDidLoad() {
        super.viewDidLoad()

        let codeLabel = NSTextField.newLabel()
        let codeMultilineLabel = NSTextField.newWrappingLabel()

        let labels = [label!, codeLabel, multilineLabel!, codeMultilineLabel]

        for keyPath in [
            "editable",
            "selectable",
            "allowsEditingTextAttributes",
            "importsGraphics",
            "textColor",
            "preferredMaxLayoutWidth",
            "backgroundColor",
            "drawsBackground",
            "bezeled",
            "bezelStyle",
            "bordered",
            "enabled",
            "alignment",
            "font",
            "lineBreakMode",
            "usesSingleLineMode",
            "formatter",
            "baseWritingDirection",
            "allowsExpansionToolTips",
            "controlSize",
            "highlighted",
            "continuous",
            "cell.opaque",
            "cell.controlTint",
            "cell.backgroundStyle",
            "cell.interiorBackgroundStyle",
            "cell.scrollable",
            "cell.truncatesLastVisibleLine",
            "cell.wraps",
            "cell.userInterfaceLayoutDirection"
        ] {
            Swift.print(keyPath + " " + labels.map({ ($0.value(forKeyPath: keyPath) as? NSObject)?.description ?? "nil" }).joined(separator: " "))
        }
    }
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
8

This can be tricky to get right. I don't have the recipe for an exact replica handy, but when I've been stuck in a similar situation, here's what I do:

  1. Create a UI element in IB.
  2. Add an outlet to it from my controller class.
  3. Break in gdb in awakeFromNib or whatever.
  4. From the gdb prompt, "p *whateverOutlet" ... this will show you the C struct contents of the label NSTextField that IB set up.

By looking at all the myriad values in there, you can get a lot of guesses about what you're neglecting to set. Usually it ends up being some magic combination of bezel and border settings, that gets you where you want to be.

danielpunkass
  • 17,527
  • 4
  • 24
  • 38
5

You could try using nib2objc to get all the properties that IB sets

g-Off
  • 105
  • 1
  • 5
3

Disassembled AppKit in Objective-C:

BOOL TMPSierraOrLater() {
    static BOOL result = NO;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        result = [NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){ 10, 12, 0 }];
    });
    return result;
}

@implementation NSTextField (TMP)

+ (instancetype)TMP_labelWithString:(NSString *)stringValue {
    if (TMPSierraOrLater()) {
        return [self labelWithString:stringValue];
    }
    NSParameterAssert(stringValue);
    NSTextField *label = [NSTextField TMP_newBaseLabelWithoutTitle];
    label.lineBreakMode = NSLineBreakByClipping;
    label.selectable = NO;
    [label setContentHuggingPriority:(NSLayoutPriorityDefaultLow + 1) forOrientation:NSLayoutConstraintOrientationHorizontal];
    [label setContentHuggingPriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
    [label setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationHorizontal];
    [label setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
    label.stringValue = stringValue;
    [label sizeToFit];
    return label;
}

+ (instancetype)TMP_wrappingLabelWithString:(NSString *)stringValue {
    if (TMPSierraOrLater()) {
        return [self wrappingLabelWithString:stringValue];
    }
    NSParameterAssert(stringValue);
    NSTextField *label = [NSTextField TMP_newBaseLabelWithoutTitle];
    label.lineBreakMode = NSLineBreakByWordWrapping;
    label.selectable = YES;
    [label setContentHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
    [label setContentHuggingPriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
    [label setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
    [label setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
    label.stringValue = stringValue;
    label.preferredMaxLayoutWidth = 0;
    [label sizeToFit];
    return label;
}

+ (instancetype)TMP_labelWithAttributedString:(NSAttributedString *)attributedStringValue {
    if (CRKSierraOrLater()) {
        return [self labelWithAttributedString:attributedStringValue];
    }
    NSParameterAssert(attributedStringValue);
    NSTextField *label = [NSTextField TMP_newBaseLabelWithoutTitle];
    [label setContentHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
    [label setContentHuggingPriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
    [label setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
    [label setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical];
    label.attributedStringValue = attributedStringValue;
    [label sizeToFit];
    return label;
}

#pragma mark - Private API

+ (instancetype)TMP_newBaseLabelWithoutTitle {
    NSTextField *label = [[self alloc] initWithFrame:CGRectZero];
    label.textColor = NSColor.labelColor;
    label.font = [NSFont systemFontOfSize:0.0];
    label.alignment = NSTextAlignmentNatural;
    label.baseWritingDirection = NSWritingDirectionNatural;
    label.userInterfaceLayoutDirection = NSApp.userInterfaceLayoutDirection;
    label.enabled = YES;
    label.bezeled = NO;
    label.bordered = NO;
    label.drawsBackground = NO;
    label.continuous = NO;
    label.editable = NO;
    return label;
}

@end
Vadim
  • 9,383
  • 7
  • 36
  • 58
2

Specifically, you will want to setBordered:NO, and set the bezel style to whatever that bezel style is which I forgot. Also setEditable:NO, and optionally setSelectable:NO. That should suffice.

Sangram Shivankar
  • 3,535
  • 3
  • 26
  • 38
Steven
  • 916
  • 10
  • 19