WWDC Quick Look đź’“ By SwiftGGTeam
Build an AppKit app with the new design

Build an AppKit app with the new design

Watch original video

Highlight

macOS Tahoe brings Liquid Glass material, floating toolbar and sidebar, new window corners and control sizes to AppKit. Most effects kick in after a rebuild with Xcode 26; the rest is handled through new APIs such as NSGlassEffectView, NSBackgroundExtensionView, and NSView.LayoutRegion.


Core Content

The most common headache when running an older AppKit app on macOS Tahoe is visual misalignment. The sidebar packs an NSVisualEffectView that blocks the new glass material entirely. Status text on the toolbar gets wrapped in a glass background automatically and looks like a fake button. The larger window corners clip a corner of the “New Folder” button sitting nearby. A dense inspector falls apart because controls grew taller overall. This is the migration reality Apple laid out at the start of the session.

Apple splits the changes from outside in into four parts (00:52): app structure (toolbar, sidebar, window corners), scroll edge effect (how scrolling content overlaps with glass elements), controls (new sizes, new shapes, prominence, glass bezel), and Liquid Glass customization (NSGlassEffectView / NSGlassEffectContainerView). Most effects take effect automatically after a rebuild in Xcode 26 — the toolbar groups itself onto glass, the sidebar floats, and controls adopt the new look. Where manual intervention is needed, each spot maps to a concrete API. There is no “rewrite the whole view” solution anywhere.


Detailed Content

The toolbar glass is applied automatically by NSToolbar (02:53). Like buttons merge into one glass piece; different types such as segmented, pop-up, and search are separated. The problem is that non-interactive content like status text and custom titles also gets glass, making them look like buttons. The fix is to turn off bordered:

// Removing toolbar item glass

toolbarItem.isBordered = false

Key points:

  • isBordered = false makes NSToolbarItem show no glass background. This suits status text in the Photos toolbar (03:11).
  • Conversely, to stress a state or important action, set style = .prominent on a toolbar item; the glass is tinted with the accent color. You can also use backgroundTintColor to specify a color (03:30).
  • To express “has unread” or “has new notification”, use NSItemBadge.count(_:), NSItemBadge.text(_:), or NSItemBadge.indicator (03:58).

The sidebar floats above content in the new design (04:12). If you want horizontally scrolling content, maps, posters, or other rich content to extend underneath the sidebar, flip one switch:

// Content under the sidebar

splitViewItem.automaticallyAdjustsSafeAreaInsets = true

Key points:

  • Set this property on the content side that is covered by the sidebar, not on the sidebar itself (05:25).
  • NSSplitView stretches the frame underneath the sidebar, then uses the safe area layout guide so content lays out only in the uncovered region.
  • For artwork that has no built-in negative space, use NSBackgroundExtensionView: fill the split item with it, give it a contentView, and AppKit automatically mirrors and blurs the extension beyond the safe area (06:24).
  • Do not forget to remove the old NSVisualEffectView from the sidebar, or it will block the new glass material (04:46).

Window corners grow larger, and buttons can hit the edge (07:07). Windows with a toolbar get larger corners that are concentric with the glass toolbar. Controls near the corner need the new layout region API:

// Avoiding a window corner


func updateConstraints() {
    guard !installedButtonConstraints else { return }

    let safeArea = layoutGuide(for: .safeArea(cornerAdaptation: .horizontal))

    NSLayoutConstraint.activate([
        safeArea.leadingAnchor.constraint(equalTo: button.leadingAnchor),
        safeArea.trailingAnchor.constraint(greaterThanOrEqualTo: button.trailingAnchor),
        safeArea.bottomAnchor.constraint(equalTo: button.bottomAnchor)
    ])
    installedButtonConstraints = true
}

Key points:

  • layoutGuide(for: .safeArea(cornerAdaptation: .horizontal)) returns a guide that adds a horizontal inset beyond the normal safe area, used to avoid the rounded corner (08:47).
  • cornerAdaptation can be .horizontal or .vertical, indicating which direction to yield to the rounded corner.
  • You can also read edge insets or the current rect directly from the layout region, for non-Auto Layout scenarios (08:17).

Scroll Edge Effect does not need to be turned on manually (09:27). NSScrollView adjusts the effect size automatically based on floating elements above it: soft-edge gradient blur, hard-edge more opaque. To hook a custom floating control into this system, use NSSplitViewItemAccessoryViewController with top/bottom aligned accessory; it affects both the scroll edge effect shape and the content safe area.

Controls gain an extra large size (11:54). There are five steps from mini to extra large. Mini, small, and medium are slightly taller than before, so do not hard-code control heights — use Auto Layout. For dense scenes (inspector, popover), set prefersCompactControlSizeMetrics = true on NSView; it inherits down the view hierarchy and controls fall back to the old height (12:56).

Control shapes: mini through medium are rounded rectangles; large and extra large are capsules. For concentric needs, override borderShape on buttons, pop-up, and segmented control (13:52). For a glass button, use the new glass bezel style and tint it with bezelColor.

Tint prominence controls tint intensity (15:31):

// Create buttons with varying levels of prominence

// Prefer a "secondary" tinted appearance for the shuffle and enqueue buttons
shuffleButton.tintProminence = .secondary
playNextButton.tintProminence = .secondary

// The "play" will automatically use primary prominence because it is the default button
playButton.keyEquivalent = "\r"

Key points:

  • tintProminence has four levels: .automatic, .none, .secondary, and .primary.
  • Secondary actions like shuffle and enqueue use .secondary to weaken the tint so they do not steal focus from the main button.
  • Mark the play button as the default button with keyEquivalent = "\r" to get primary prominence automatically.
  • This also works on sliders: tintProminence = .none leaves the track unfilled. The new neutralValue lets you anchor the fill anywhere on the track, for example 1x for playback speed (16:20).

Liquid Glass adoption is two steps (18:13). First, wrap the elements that should turn to glass in NSGlassEffectView:

// Adopting NSGlassEffectView

let userInfoView = UserInfoView()
let activityPickerView = ActivityPickerView()

let userInfoGlass = NSGlassEffectView()
userInfoGlass.contentView = userInfoView

let activityPickerGlass = NSGlassEffectView()
activityPickerGlass.contentView = activityPickerView

let stack = NSStackView(views: [userInfoGlass, 
                                activityPickerGlass])
stack.orientation = .horizontal

Key points:

  • Set the view to be “glassed” to contentView; AppKit automatically binds the glass geometry to the content with Auto Layout and applies readability-preserving visual treatment (18:42).
  • Do not place NSGlassEffectView as a sibling behind the content; it must wrap the content from outside.
  • cornerRadius and tint color are adjustable on NSGlassEffectView.

Adjacent glass elements must be grouped with NSGlassEffectContainerView:

// Adopting NSGlassEffectContainerView

let userInfoView = UserInfoView()
let activityPickerView = ActivityPickerView()

let userInfoGlass = NSGlassEffectView()
userInfoGlass.contentView = userInfoView
userInfoGlass.cornerRadius = 999

let activityPickerGlass = NSGlassEffectView()
activityPickerGlass.contentView = activityPickerView
activityPickerGlass.cornerRadius = 999

let stack = NSStackView(views: [userInfoGlass, 
                                activityPickerGlass])
stack.orientation = .horizontal

let glassContainer = NSGlassEffectContainerView()
glassContainer.contentView = stack

Key points:

  • NSGlassEffectContainerView merges multiple glass pieces into one render pass; when close, the glass can fuse liquid-like (19:35).
  • The spacing property controls the fusion distance threshold.
  • The glass material samples the surroundings to generate refraction, but glass cannot sample another piece of glass — ungrouped pieces show color inconsistency; grouping lets multiple glass pieces share one sample (20:23).
  • The container binds to contentView with Auto Layout just the same, so it can replace an existing layout node in place (21:03).

Core Takeaways

1. Rebuild with Xcode 26 first, observe default behavior, then decide what to change

Why it matters: the toolbar groups onto glass automatically, the sidebar floats automatically, and controls resize automatically. Most visual updates are free. Spot what breaks first, then fix it surgically — far cheaper than rewriting every screen upfront.

How to start: switch to Xcode 26, build the current project, launch, and screenshot each window. Compare against the old screenshots and mark spots that “changed automatically but changed wrong.”

2. Audit hard-coded control heights and switch them all to Auto Layout

Why it matters: mini, small, and medium controls all grew taller, and extra large is new. Hard-coded heightAnchor.constraint(equalToConstant:) is the densest source of breakage in this migration. Inspector, popover, and menu item layouts are all affected.

How to start: grep the project for heightAnchor.constraint(equalToConstant, frame.size.height =, and bounds.height =. Replace all control-related ones. For old screens that must stay dense, set prefersCompactControlSizeMetrics = true on the parent view.

3. Add symbol icons to menu items and clean out NSVisualEffectView from the sidebar

Why it matters: the new design encourages symbols throughout menus, which directly speeds up scanning. Residual visual effect views in the sidebar block the new glass material and are the most common culprit behind “this does not look like a Tahoe app.”

How to start: list all menu items, check off those that have a matching SF Symbol, and fill the rest. Search for NSVisualEffectView in the view debugger and confirm all instances inside the sidebar are removed.

4. Use Liquid Glass only for top-level controls, and group them with Container

Why it matters: glass marks the layer of “controls and navigation floating above content.” Apple explicitly recommends limiting its use. Large or isolated glass areas suffer color inconsistency from sampling conflicts and cost more performance.

How to start: find the toolbars and editing controls that float above content in the current UI and wrap them in NSGlassEffectView. Whenever a second glass piece sits nearby, wrap the whole set in NSGlassEffectContainerView and tune spacing to the right fusion distance.

5. Use NSBackgroundExtensionView as a fallback when rich content extends under the sidebar

Why it matters: posters, maps, and similar artwork have no negative space for the sidebar. Forcing the extension cuts off key information. Background Extension View automatically mirrors and blurs the extended area, so the real content inside the safe area stays unchanged.

How to start: first turn on splitViewItem.automaticallyAdjustsSafeAreaInsets = true. For assets without negative space, wrap them in NSBackgroundExtensionView, put only the real image in contentView, and let the system generate the extension.


Comments

GitHub Issues · utterances