Comprehensive macOS AppKit Auto Layout guide covering NSView programmatic constraints, NSStackView, debugging, and best practices. Use when building native macOS apps with AppKit Auto Layout.
Scope: macOS/AppKit only. All code is Swift. All views are NSView, not UIView.
Every constraint is a linear equation:
view1.attribute = multiplier × view2.attribute + constant
Seven components: Item1, Attribute1, Relationship (=, ≥, ≤), Multiplier, Item2, Attribute2, Constant.
Attribute categories:
Rules:
leading/trailing over left/right (RTL support)Priority values (macOS-specific):
| Value | Constant | Purpose |
|---|---|---|
| 1000 | .required | Must be satisfied |
| 750 | .defaultHigh | Default Compression Resistance |
| 510 | .dragThatCanResizeWindow | macOS only — drag that resizes window |
| 500 | .windowSizeStayPut | macOS only — window stays current size |
| 490 | .dragThatCannotResizeWindow | macOS only — drag that cannot resize window |
| 250 | .defaultLow | Default Content Hugging |
| 50 | .fittingSizeCompression | Used by fittingSize calculation |
let label = NSTextField(labelWithString: "Hello")
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 16),
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
])
Type safety — Anchor subclasses prevent invalid constraints at compile time:
NSLayoutXAxisAnchor (leading, trailing, centerX, left, right)NSLayoutYAxisAnchor (top, bottom, centerY, firstBaseline, lastBaseline)NSLayoutDimension (width, height) — only dimension anchors support multiplier// ✅ Compiles — both are X-axis
label.leadingAnchor.constraint(equalTo: view.trailingAnchor)
// ❌ Won't compile — X-axis vs Y-axis
label.leadingAnchor.constraint(equalTo: view.topAnchor)
// ✅ Dimension with multiplier
view2.widthAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 0.5)
NSLayoutConstraint(
item: subview,
attribute: .leading,
relatedBy: .equal,
toItem: view,
attribute: .leadingMargin,
multiplier: 1.0,
constant: 0.0
).isActive = true
Only use when: you need a multiplier on non-dimension attributes (rare). Downsides: No type safety, 7 params, verbose.
let views = ["btn1": button1, "btn2": button2]
let metrics = ["sp": 8.0]
let constraints = NSLayoutConstraint.constraints(
withVisualFormat: "H:[btn1]-sp-[btn2]",
options: .alignAllBaseline,
metrics: metrics,
views: views
)
NSLayoutConstraint.activate(constraints)
Downsides: No compile-time checks, no multiplier, no aspect ratio, no baseline alignment.
// ⚠️ CRITICAL: Set to false for EVERY programmatically created view
let myView = NSView()
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
true — the system auto-generates constraints from the autoresizing maskfalse, you WILL get conflictsfalse automaticallyclass MyView: NSView {
// 1. Update phase — modify constraints here
override func updateConstraints() {
// Modify constraints based on current state
// ⚠️ Call super LAST (opposite of most overrides!)
super.updateConstraints()
}
// 2. Layout phase — frame is set here, adjust subviews
override func layout() {
super.layout() // ← Call super FIRST
// Frame-based adjustments after layout
}
// 3. Display phase — drawing
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
}
Triggering layout updates:
view.needsUpdateConstraints = true // Schedule constraint update
view.needsLayout = true // Schedule layout pass
view.layoutSubtreeIfNeeded() // Force immediate layout (entire subtree)
view.invalidateIntrinsicContentSize() // Recalculate intrinsic size
// Check if layout is ambiguous
view.hasAmbiguousLayout
// Get all constraints affecting a specific axis
view.constraintsAffectingLayout(for: .horizontal) // NSLayoutConstraint.Orientation
view.constraintsAffectingLayout(for: .vertical)
// Jump between ambiguous solutions (debug only)
view.exerciseAmbiguityInLayout()
// fittingSize — minimum size that satisfies all constraints
view.fittingSize
⚠️ macOS uses
NSLayoutConstraint.Orientation(.horizontal, .vertical), NOTUILayoutConstraintAxis.
// ✅ Recommended — batch activation, better performance
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
view.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
view.topAnchor.constraint(equalTo: parent.topAnchor),
view.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
])
// Deactivation
NSLayoutConstraint.deactivate([constraint1, constraint2])
// ❌ Avoid — individual activation is slower
view.addConstraint(constraint) // Legacy API
constraint.isActive = true // One at a time
// ✅ Can modify after creation
constraint.constant = 20.0
constraint.priority = .defaultHigh // ⚠️ See priority rules below
constraint.identifier = "sidebar-width"
// ❌ Immutable — must remove and recreate
// constraint.multiplier
// constraint.firstItem / secondItem
// constraint.firstAttribute / secondAttribute
// constraint.relation
// ❌ CRASH: Cannot change from/to .required (1000)
let c = view.widthAnchor.constraint(equalToConstant: 200)
c.priority = .required
c.isActive = true
c.priority = .defaultHigh // 💥 Runtime crash
// ✅ Use 999 instead of 1000 if you need to change later
let c = view.widthAnchor.constraint(equalToConstant: 200)
c.priority = NSLayoutConstraint.Priority(999)
c.isActive = true
c.priority = .defaultHigh // ✅ Fine
let leading = label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16)
leading.identifier = "Label.leading"
When constraints conflict, identifiers appear in the console log instead of memory addresses.
// ❌ Old pattern: invisible spacer views (waste of resources)
let spacer = NSView()
spacer.isHidden = true
// ✅ Modern pattern: layout guides (lightweight, no rendering)
let spacer = NSLayoutGuide()
view.addLayoutGuide(spacer)
NSLayoutConstraint.activate([
spacer.leadingAnchor.constraint(equalTo: view1.trailingAnchor),
spacer.trailingAnchor.constraint(equalTo: view2.leadingAnchor),
spacer.widthAnchor.constraint(equalToConstant: 20),
])
Use guides for: equal spacing, alignment groups, invisible layout regions.
Some views know their natural size based on content (NSTextField, NSButton, NSImageView).
class MyCustomView: NSView {
var content: String = "" {
didSet {
invalidateIntrinsicContentSize() // ⚠️ Must call when content changes!
}
}
override var intrinsicContentSize: NSSize {
let size = calculateContentSize()
return NSSize(
width: size.width > 0 ? size.width : NSView.noIntrinsicMetric,
height: size.height > 0 ? size.height : NSView.noIntrinsicMetric
)
}
}
⚠️
intrinsicContentSizemust NOT depend onframe(frame may not be set yet). ReturnNSView.noIntrinsicMetricfor dimensions with no natural size.
// Two labels side by side — which stretches when there's extra space?
// → The one with LOWER content hugging
label1.setContentHuggingPriority(.defaultHigh, for: .horizontal) // 750 — stays tight
label2.setContentHuggingPriority(.defaultLow, for: .horizontal) // 250 — stretches
// Which gets truncated when space is tight?
// → The one with LOWER compression resistance
label1.setContentCompressionResistancePriority(.required, for: .horizontal) // 1000
label2.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) // 750
NSTextField may need preferredMaxLayoutWidth for correct multi-line height calculation:
textField.preferredMaxLayoutWidth = 300 // Wrap at this width
| Feature | NSStackView (macOS) | UIStackView (iOS) |
|---|---|---|
| Direction property | orientation | axis |
| Has gravity areas | ✅ Yes (top/leading, center, bottom/trailing) | ❌ No |
detachesHiddenViews | ✅ Default true — hidden views removed from hierarchy | ❌ N/A |
| Visibility priority | ✅ Per-view priority for clipping order | ❌ N/A |
| Default rendering | Is a layer (renders itself) | Not a rendering view |
| Adding views | addView(_:in:) or addArrangedSubview(_:) | addArrangedSubview(_:) |
let stack = NSStackView()
stack.orientation = .horizontal
// Add views to specific gravity areas
stack.addView(leftButton, in: .leading) // Pinned to leading edge
stack.addView(titleLabel, in: .center) // Centered
stack.addView(closeButton, in: .trailing) // Pinned to trailing edge
stack.distribution = .fill // Default — views fill based on hugging/resistance
stack.distribution = .fillEqually // All views same size
stack.distribution = .fillProportionally // Proportional to intrinsic size
stack.distribution = .equalSpacing // Equal spacing between views
stack.distribution = .equalCentering // Equal spacing between view centers
stack.distribution = .gravityAreas // Uses gravity areas (macOS unique!)
⚠️ When using
.gravityAreas, add views withaddView(_:in:). When using other distributions, useaddArrangedSubview(_:).
// ⚠️ Default is TRUE on macOS!
// When you set view.isHidden = true, NSStackView REMOVES it from the view hierarchy
stackView.detachesHiddenViews = true // default
// If you depend on hidden views staying in the hierarchy:
stackView.detachesHiddenViews = false
// Controls which views get clipped first when space is tight
stackView.setVisibilityPriority(.mustHold, for: importantView) // Never clip
stackView.setVisibilityPriority(.detachOnlyIfNecessary, for: optionalView) // Clip if needed
stackView.setVisibilityPriority(.notVisible, for: hiddenView) // Always hidden
stackView.setCustomSpacing(20, after: headerView)
NSScrollView
└─ NSClipView (contentView)
└─ documentView (your content)
└─ NSScroller (vertical)
└─ NSScroller (horizontal)
let scrollView = NSScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
let contentView = NSView()
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.documentView = contentView
NSLayoutConstraint.activate([
// ScrollView pinned to parent
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// ⚠️ macOS: documentView constrained to NSClipView
contentView.leadingAnchor.constraint(equalTo: scrollView.contentView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.contentView.trailingAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
// ❌ Do NOT pin bottom to clipView (that prevents scrolling)
// Instead, let content's own height constraints define scrollable area
])
When using titlebarAppearsTransparent or fullSizeContentView, content extends behind the title bar. Use contentLayoutGuide to avoid overlap:
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
let contentView = window.contentView!
// Background extends behind title bar
let bg = NSVisualEffectView()
bg.translatesAutoresizingMaskIntoConstraints = false
bg.material = .sidebar
contentView.addSubview(bg)
NSLayoutConstraint.activate([
bg.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
bg.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
bg.topAnchor.constraint(equalTo: contentView.topAnchor),
bg.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
// ⚠️ Main content uses contentLayoutGuide to avoid title bar area
if let guide = window.contentLayoutGuide as? NSLayoutGuide {
let mainView = NSView()
mainView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(mainView)
NSLayoutConstraint.activate([
mainView.topAnchor.constraint(equalTo: guide.topAnchor, constant: 8),
mainView.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 8),
mainView.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -8),
mainView.bottomAnchor.constraint(equalTo: guide.bottomAnchor, constant: -8),
])
}
⚠️
window.contentLayoutGuideis typed asAny?— must cast toNSLayoutGuide.
Controls which panel resizes when the window size changes:
let sidebar = NSSplitViewItem(sidebarWithViewController: sidebarVC)
sidebar.minimumThickness = 200
sidebar.maximumThickness = 400
sidebar.holdingPriority = NSLayoutConstraint.Priority(260) // Low — resizes easily
let content = NSSplitViewItem(viewController: contentVC)
content.holdingPriority = NSLayoutConstraint.Priority(490) // Higher — resists resizing
let inspector = NSSplitViewItem(viewController: inspectorVC)
inspector.minimumThickness = 200
inspector.canCollapse = true
inspector.holdingPriority = NSLayoutConstraint.Priority(250) // Lowest — resizes first
splitViewController.splitViewItems = [sidebar, content, inspector]
Higher holdingPriority → panel resists resizing more → other panels absorb size changes first.
// Toggle sidebar collapse
let item = splitViewController.splitViewItems[0]
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.25
item.animator().isCollapsed.toggle()
}
class MyViewController: NSViewController {
override func loadView() {
self.view = NSView() // ⚠️ Must set self.view if not using nib
}
override func viewDidLoad() {
super.viewDidLoad()
// Create and constrain subviews here
}
override func viewWillAppear() {
super.viewWillAppear()
// View is about to be displayed (macOS 10.10+)
}
override func viewDidAppear() {
super.viewDidAppear()
// View is now on screen; window and frame are valid
}
override func viewWillLayout() {
super.viewWillLayout()
// About to layout — adjust constraints if needed
}
override func viewDidLayout() {
super.viewDidLayout()
// Layout complete — frames are final
}
}
⚠️ macOS gotcha: If
nibNameandbundleare both nil (macOS 10.10+), AppKit looks for a nib matching the class name. OverrideloadView()to create views programmatically.
| Aspect | macOS (AppKit) | iOS (UIKit) |
|---|---|---|
| Base view | NSView | UIView |
| Coordinate origin | Bottom-left | Top-left |
| Layout method | layout() | layoutSubviews() |
| Needs layout | needsLayout = true | setNeedsLayout() |
| Force layout | layoutSubtreeIfNeeded() | layoutIfNeeded() |
| Update constraints | needsUpdateConstraints = true | setNeedsUpdateConstraints() |
| Stack view direction | .orientation | .axis |
| Orientation enum | NSUserInterfaceLayoutOrientation | NSLayoutConstraint.Axis |
| Constraint orientation | NSLayoutConstraint.Orientation | NSLayoutConstraint.Axis |
| Layout guide | NSLayoutGuide | UILayoutGuide |
| Safe area | safeAreaLayoutGuide (macOS 11+) | safeAreaLayoutGuide (iOS 11+) |
| Window layout guide | NSWindow.contentLayoutGuide | N/A |
| Split view priority | holdingPriority | N/A |
| Stack detaches hidden | detachesHiddenViews = true (default) | N/A |
Console output:
Unable to simultaneously satisfy constraints.
(
"<NSLayoutConstraint:0x... V:|-(20)-[label] (active, names: '|':NSView:0x...)>",
"<NSLayoutConstraint:0x... V:|-(30)-[label] (active)>"
)
Will attempt to recover by breaking constraint ...
Fix checklist:
translatesAutoresizingMaskIntoConstraints = false?constraint.identifier to find the offending constraint.required to 999// Detect
po view.hasAmbiguousLayout
// Find all ambiguous views
func findAmbiguous(in view: NSView) {
if view.hasAmbiguousLayout {
print("⚠️ Ambiguous: \(view)")
print(" H: \(view.constraintsAffectingLayout(for: .horizontal))")
print(" V: \(view.constraintsAffectingLayout(for: .vertical))")
}
view.subviews.forEach { findAmbiguous(in: $0) }
}
// Toggle between valid solutions
view.exerciseAmbiguityInLayout()
Layout is valid but wrong. Use Xcode's Debug View Hierarchy (Debug → View Debugging → Capture View Hierarchy).
Add in Xcode: Symbol = NSViewAlertForUnsatisfiableConstraints
This pauses execution at the exact moment a conflict is detected.
// ❌ Constraints + autoresizing mask = conflict
let label = NSTextField(labelWithString: "Hi")
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true // 💥
// ✅
let label = NSTextField(labelWithString: "Hi")
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true // ✅
// ❌ Can cause infinite layout loops
override func layout() {
super.layout()
someConstraint.constant = bounds.width / 2 // Triggers another layout pass!
}
// ✅ Modify constraints in updateConstraints()
override func updateConstraints() {
someConstraint.constant = calculatedValue
super.updateConstraints() // ⚠️ Call super LAST
}
// ⚠️ Default is true — hidden views are REMOVED from the view hierarchy
stackView.detachesHiddenViews = true // default!
// If you need hidden views to stay in the hierarchy:
stackView.detachesHiddenViews = false
// ❌ Crash
constraint.priority = .required // 1000
constraint.isActive = true
constraint.priority = .defaultHigh // 💥
// ✅ Start at 999
constraint.priority = NSLayoutConstraint.Priority(999)
constraint.isActive = true
constraint.priority = .defaultHigh // ✅
// ❌ Compile error — contentLayoutGuide is Any?
view.topAnchor.constraint(equalTo: window.contentLayoutGuide.topAnchor)
// ✅ Cast first
if let guide = window.contentLayoutGuide as? NSLayoutGuide {
view.topAnchor.constraint(equalTo: guide.topAnchor)
}
// ❌ Semantic mismatch
view.leadingAnchor.constraint(equalTo: other.leftAnchor)
// ✅ Be consistent
view.leadingAnchor.constraint(equalTo: other.leadingAnchor)
// ❌ Slower, less efficient
constraint1.isActive = true
constraint2.isActive = true
constraint3.isActive = true
// ✅ Batch activation
NSLayoutConstraint.activate([constraint1, constraint2, constraint3])
// ❌ Pinning documentView bottom to clipView prevents scrolling
contentView.bottomAnchor.constraint(equalTo: scrollView.contentView.bottomAnchor)
// ✅ Let content height be determined by its own subviews
// Only pin top, leading, trailing to clipView
// Content's internal constraints determine scrollable height
// ❌ Content changes but layout doesn't update
class MyView: NSView {
var text: String = "" {
didSet { /* nothing */ }
}
}
// ✅
class MyView: NSView {
var text: String = "" {
didSet { invalidateIntrinsicContentSize() }
}
}
class MyViewController: NSViewController {
private let headerLabel = NSTextField(labelWithString: "Title")
private let contentView = NSView()
private let footerButton = NSButton(title: "Done", target: nil, action: nil)
override func loadView() {
self.view = NSView()
}
override func viewDidLoad() {
super.viewDidLoad()
[headerLabel, contentView, footerButton].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
NSLayoutConstraint.activate([
headerLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 16),
headerLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
headerLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
contentView.topAnchor.constraint(equalTo: headerLabel.bottomAnchor, constant: 12),
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
footerButton.topAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 12),
footerButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
footerButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
])
}
}
class AdaptiveViewController: NSViewController {
private var compactConstraints: [NSLayoutConstraint] = []
private var regularConstraints: [NSLayoutConstraint] = []
override func viewDidLoad() {
super.viewDidLoad()
let sidebar = NSView()
let content = NSView()
[sidebar, content].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
// Compact: stacked vertically
compactConstraints = [
sidebar.topAnchor.constraint(equalTo: view.topAnchor),
sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sidebar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
sidebar.heightAnchor.constraint(equalToConstant: 200),
content.topAnchor.constraint(equalTo: sidebar.bottomAnchor),
content.leadingAnchor.constraint(equalTo: view.leadingAnchor),
content.trailingAnchor.constraint(equalTo: view.trailingAnchor),
content.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]
// Regular: side by side
regularConstraints = [
sidebar.topAnchor.constraint(equalTo: view.topAnchor),
sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sidebar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
sidebar.widthAnchor.constraint(equalToConstant: 250),
content.topAnchor.constraint(equalTo: view.topAnchor),
content.leadingAnchor.constraint(equalTo: sidebar.trailingAnchor),
content.trailingAnchor.constraint(equalTo: view.trailingAnchor),
content.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]
NSLayoutConstraint.activate(regularConstraints)
}
func switchToCompact() {
NSLayoutConstraint.deactivate(regularConstraints)
NSLayoutConstraint.activate(compactConstraints)
}
func switchToRegular() {
NSLayoutConstraint.deactivate(compactConstraints)
NSLayoutConstraint.activate(regularConstraints)
}
}
func expandPanel() {
panelWidthConstraint.constant = 300
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.3
context.allowsImplicitAnimation = true
view.layoutSubtreeIfNeeded() // ← macOS: layoutSubtreeIfNeeded, NOT layoutIfNeeded
}
}
let buttons = (1...4).map { NSButton(title: "Btn \($0)", target: nil, action: nil) }
let spacers = (0...4).map { _ -> NSLayoutGuide in
let guide = NSLayoutGuide()
view.addLayoutGuide(guide)
return guide
}
buttons.forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
var constraints: [NSLayoutConstraint] = []
constraints.append(spacers[0].leadingAnchor.constraint(equalTo: view.leadingAnchor))
for (i, button) in buttons.enumerated() {
constraints.append(button.leadingAnchor.constraint(equalTo: spacers[i].trailingAnchor))
constraints.append(button.centerYAnchor.constraint(equalTo: view.centerYAnchor))
constraints.append(spacers[i + 1].leadingAnchor.constraint(equalTo: button.trailingAnchor))
if i > 0 {
constraints.append(spacers[i].widthAnchor.constraint(equalTo: spacers[0].widthAnchor))
}
}
constraints.append(spacers[4].trailingAnchor.constraint(equalTo: view.trailingAnchor))
constraints.append(spacers[4].widthAnchor.constraint(equalTo: spacers[0].widthAnchor))
NSLayoutConstraint.activate(constraints)
class FormViewController: NSViewController {
override func loadView() { self.view = NSView() }
override func viewDidLoad() {
super.viewDidLoad()
let form = NSStackView()
form.orientation = .vertical
form.alignment = .leading
form.spacing = 12
form.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(form)
NSLayoutConstraint.activate([
form.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
form.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
form.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
])
// Each row is a horizontal stack
for label in ["Name:", "Email:", "Phone:"] {
let row = NSStackView()
row.orientation = .horizontal
row.spacing = 8
row.alignment = .firstBaseline
let labelField = NSTextField(labelWithString: label)
labelField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
labelField.widthAnchor.constraint(equalToConstant: 80).isActive = true
let input = NSTextField()
input.placeholderString = "Enter \(label.dropLast())"
input.setContentHuggingPriority(.defaultLow, for: .horizontal)
row.addArrangedSubview(labelField)
row.addArrangedSubview(input)
form.addArrangedSubview(row)
row.widthAnchor.constraint(equalTo: form.widthAnchor).isActive = true
}
}
}
When layout breaks, check in this order:
translatesAutoresizingMaskIntoConstraints = false.required constraintsleading/trailing consistently (not mixed with left/right).identifier setcontentMinSize is reasonablecontentLayoutGuidelayout() (use updateConstraints())Every view needs 4 constraints (or equivalent via intrinsic content size):
NSLayoutConstraint.activate([
child.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
child.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
child.topAnchor.constraint(equalTo: parent.topAnchor),
child.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
])
NSLayoutConstraint.activate([
child.centerXAnchor.constraint(equalTo: parent.centerXAnchor),
child.centerYAnchor.constraint(equalTo: parent.centerYAnchor),
])
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: 200),
view.heightAnchor.constraint(equalToConstant: 100),
])
view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: 16.0/9.0).isActive = true
| API | macOS Version |
|---|---|
| NSLayoutConstraint | 10.7+ |
| NSStackView | 10.9+ |
| NSLayoutGuide | 10.11+ |
| NSLayoutAnchor | 10.11+ |
| NSStackView.detachesHiddenViews | 10.11+ |
| NSWindow.contentLayoutGuide | 10.10+ |
| safeAreaLayoutGuide | 11.0+ |