Sample code for creating a NSTextField "label"?

Objective CCocoaUser InterfaceMacos

Objective C Problem Overview


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.

Objective C Solutions


Solution 1 - Objective C

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];
}

Solution 2 - Objective C

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: " "))
        }
    }
}

Solution 3 - Objective C

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.

Solution 4 - Objective C

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

Solution 5 - Objective C

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.

Solution 6 - Objective C

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTodd DitchendorfView Question on Stackoverflow
Solution 1 - Objective CThibault Martin-LagardetteView Answer on Stackoverflow
Solution 2 - Objective Crob mayoffView Answer on Stackoverflow
Solution 3 - Objective CdanielpunkassView Answer on Stackoverflow
Solution 4 - Objective Cg-OffView Answer on Stackoverflow
Solution 5 - Objective CStevenView Answer on Stackoverflow
Solution 6 - Objective CVadimView Answer on Stackoverflow