Skip to content
GitHub

Layout and content insets

The expanded panel’s glass width and the content column inside it are controlled by different knobs on different layers. Stacking horizontal padding - for example .padding(.horizontal, 12) on a home view when the framework already applies metrics.edgePadding - is the most common cause of dead space beside answer text and bottom command rows.

This guide maps each knob, shows how they compose, and points at Examples/LayoutNook/main.swift for a working host pattern.

Expanded layout stacks three responsibilities:

  1. NookSurface (NookView) - clips the notch shape, applies NookStyle.expandedContentInsets as .safeAreaInset strips, adds structural horizontal padding (topCornerRadius) for the panel ears, and derives the outer nookContentInsets from corner radii.
  2. NookKit (NookExpandedView) - pins the inner VStack to expandedWidth, wraps it in metrics.edgePadding, and re-injects reduced nookContentInsets for descendants.
  3. Host views - read @Environment(\.nookContentInsets) when content pins to an edge or corner; use frame(maxWidth: .infinity, …) when a row should span the full content column.
┌─ NookShape clip (topCornerRadius / bottomCornerRadius) ─────────────────┐
│ .padding(.horizontal, topCornerRadius) ← structural ear clearance │
│ ┌─ expandedContent HStack ────────────────────────────────────────────┐ │
│ │ safeAreaInset: expandedContentInsets (default 0/8/8/8) │ │
│ │ ┌─ NookExpandedView ─────────────────────────────────────────────┐ │ │
│ │ │ .padding(edgePadding) ← default 8 pt │ │ │
│ │ │ ┌─ VStack .frame(width: expandedWidth ?? 520) ──────────────┐ │ │ │
│ │ │ │ env: nookContentInsets (reduced by edgePadding) │ │ │ │
│ │ │ │ → top bar, Settings, host home │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
Knob Layer What it does
NookConfiguration.expandedWidth NookKit Fixed width of the inner VStack (default 520). Does not resize the glass by itself - the surface measures the wrapped chrome.
NookChromeMetrics.edgePadding NookKit Padding around the inner VStack inside the expanded surface. Default 8 (NookLayout.edgePadding). Subtracted before re-injecting nookContentInsets.
NookStyle.expandedContentInsets NookSurface Per-edge .safeAreaInset strip the chrome reserves around expanded content. Default: top 0, other edges 8.
NookStyle.topCornerRadius / bottomCornerRadius NookSurface Panel corner radii; drive clip shape and nookContentInsets derivation.
EnvironmentValues.nookContentInsets Surface -> host Residual curve clearance relative to the host content frame. Use for edge/corner-pinned layout - not as a substitute for centering.
NookTopBarConfiguration.notchClearance NookKit Whether expanded content starts below the hardware notch (.automatic, the default) or may run up beside it (.manual). See Clearing the notch.
EnvironmentValues.nookNotchCutout Surface -> host Where the hardware notch falls in the host content frame. .none in the floating form.

expandedContentInsets and edgePadding both default to 8 pt on the sides for historical parity, but they are independent knobs on different layers.

At the framework defaults - expandedWidth = 520, radii top 19 / bottom 24, expandedContentInsets (0, 8, 8, 8), edgePadding = 8:

Step 1 - outer nookContentInsets (injected by NookView):

Edge Residual
top 19
bottom 16
leading 16
trailing 16

Step 2 - inner nookContentInsets (after NookExpandedView subtracts edgePadding):

Edge Residual
top 11
bottom 8
leading 8
trailing 8

Step 3 - edge-aligned text width inside the 520 pt VStack:

520 − leading(8) − trailing(8) = 504 pt

SettingsView and NookTopBar follow this pattern - section labels and icon clusters pad by contentInsets.leading / contentInsets.trailing so everything shares one left margin.

Wider panel (600 pt, trimmed bottom inset)

Section titled “Wider panel (600 pt, trimmed bottom inset)”

LayoutNook sets expandedWidth = 600 and tightens only the bottom chrome strip (expandedContentInsets.bottom = 2, sides unchanged):

  • Inner VStack width: 600 pt
  • Edge-aligned usable width: 600 − 8 − 8 = 584 pt
  • nookContentInsets.bottom rises to 22 (24 − 2) - bottom-corner content must inset more; horizontally-centered rows are unaffected and sit ~6 pt closer to the rounded bottom.

Set width and trim chrome insets in configuration

Section titled “Set width and trim chrome insets in configuration”
var configuration = NookConfiguration()
configuration.expandedWidth = 600
configuration.style = NookStyle(
topCornerRadius: 19,
bottomCornerRadius: 24,
expandedContentInsets: NookEdgeInsets(top: 0, bottom: 2, leading: 8, trailing: 8)
)
configuration.setHome { MyHomeView() }
NookApp.main(configuration)

Leave metrics.edgePadding at the default unless you have a deliberate reason to change the wrapper padding - most hosts only need expandedWidth and expandedContentInsets.

struct MyHomeView: View {
@Environment(\.nookContentInsets) private var contentInsets
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Answer text …")
.padding(.leading, contentInsets.leading)
.padding(.trailing, contentInsets.trailing)
Spacer(minLength: 0)
commandRow
}
}
private var commandRow: some View {
HStack { /* … */ }
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, contentInsets.leading)
.padding(.trailing, contentInsets.trailing)
.padding(.bottom, contentInsets.bottom)
}
}

Do not add .padding(.horizontal, 12) (or any fixed horizontal padding) on the home root - NookExpandedView already applies metrics.edgePadding. Extra padding narrows the effective column and leaves visible dead space beside text and toolbars.

Bottom toolbars that should span the content column need two things:

  1. frame(maxWidth: .infinity, alignment: .leading) on the row - otherwise SwiftUI shrink-wraps the HStack to its buttons and the row looks narrower than the panel.
  2. Edge insets from nookContentInsets - not a second horizontal padding pass.

Centered command rows (icons in the middle of the panel) can ignore horizontal nookContentInsets; only corner-pinned content needs the leading/trailing values.

  • Centered placeholder content (title + subtitle in the middle) - the default demo home does this with vertical padding only.
  • Full-bleed custom art that intentionally reaches toward the curves - rare; test on a notched display.

When content aligns with Settings rows or the top-bar leading cluster, always use nookContentInsets.

// Avoid — stacks on top of NookExpandedView's edgePadding (8) and any
// expandedContentInsets the chrome already consumed.
VStack { }
.padding(.horizontal, 12)

If the panel feels narrower than the glass, check for fixed horizontal padding on the registered home view before tuning expandedWidth.

In the notch form the panel’s top edge is the top of the screen, so the camera housing covers the middle of a band across the top of the expanded content. The top bar fills that band: its title sits left of the notch, and the lock and gear sit right of it. Anything else drawn in the middle of the band is hidden.

NookTopBarConfiguration.notchClearance decides what the content does about it:

Value Top bar showing Top bar hidden
.automatic (default) The bar grows to fill the band when the notch is taller than metrics.topBarHeight. The content starts where it would below the bar. The band stays empty, apart from views placed there with nookNotchAccessories(leading:trailing:).
.manual The content starts right below the bar. The content starts at the top of the panel, beside and under the notch. Lay it out around the notch yourself.

The floating form has no notch, so both values lay out the same there.

Apply nookNotchAccessories(leading:trailing:) to the outermost view of your home view’s body to put small views, such as icons and buttons, on either side of the notch:

struct PlayerHome: View {
var body: some View {
PlayerView()
.nookNotchAccessories {
Image(systemName: "music.note")
} trailing: {
NookKeepOpenButton()
}
}
}
var configuration = NookConfiguration()
configuration.topBar.showsTopBar = false
configuration.setHome { PlayerHome() }

With .automatic the accessories take the band and the rest of the view starts below the notch. With .manual they fill the band at the top of the view and the content follows them. Where there is no band to take, below the top bar or in the floating form, they are the view’s first row. The modifier goes inside a view type because setHome(_:) takes a Sendable view, which a modified view is not.

Each side only gets the width between the content’s edge and the notch: about 150 pt at the default 520 pt width, and about 80 pt at 380 pt, beside a 185 pt notch. Wrap a label in ViewThatFits to fall back to an icon when it does not fit, as Examples/PlaygroundNook does.

@Environment(\.nookNotchCutout) describes the notch relative to the view that reads it: its width, how far it reaches down into the frame (height), and how far its center sits from the frame’s center (centerOffset). It is .none in the floating form. NookNotchRow uses it to split a row around the notch:

NookNotchRow {
Label("Today", systemImage: "sun.max")
} trailing: {
Button("Refresh", systemImage: "arrow.clockwise", action: refresh)
}

Like nookContentInsets, the value is relative to the reading view’s frame. A wrapper that pads its content should re-inject an adjusted copy made with insetBy(top:leading:trailing:).

  • Examples/LayoutNook/main.swift - working 600 pt host with trimmed bottom inset and a full-width command row.
  • Theming - corner radii and expandedContentInsets in context.
  • Settings chrome - top-bar layout that shares the same inset contract.
  • Sources/NookKit/App/Views/Settings/SettingsView.swift - canonical edge-aligned list layout inside the framework.
  • Tests/NookKitTests/NookContentInsetsTests.swift - executable spec for inset derivation math.
  • Tests/NookKitTests/NookNotchClearanceTests.swift - where the content, the top bar, and notch accessories land around the notch.