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.
Three layers
Section titled “Three layers”Expanded layout stacks three responsibilities:
NookSurface(NookView) - clips the notch shape, appliesNookStyle.expandedContentInsetsas.safeAreaInsetstrips, adds structural horizontal padding (topCornerRadius) for the panel ears, and derives the outernookContentInsetsfrom corner radii.NookKit(NookExpandedView) - pins the inner VStack toexpandedWidth, wraps it inmetrics.edgePadding, and re-injects reducednookContentInsetsfor descendants.- Host views - read
@Environment(\.nookContentInsets)when content pins to an edge or corner; useframe(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 reference
Section titled “Knob reference”| 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.
Usable content width (worked example)
Section titled “Usable content width (worked example)”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 ptSettingsView 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.bottomrises to 22 (24 − 2) - bottom-corner content must inset more; horizontally-centered rows are unaffected and sit ~6 pt closer to the rounded bottom.
Recommended host patterns
Section titled “Recommended host patterns”Set width and trim chrome insets in configuration
Section titled “Set width and trim chrome insets in configuration”var configuration = NookConfiguration()configuration.expandedWidth = 600configuration.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.
Read nookContentInsets in the home view
Section titled “Read nookContentInsets in the home view”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.
Full-width bottom command rows
Section titled “Full-width bottom command rows”Bottom toolbars that should span the content column need two things:
frame(maxWidth: .infinity, alignment: .leading)on the row - otherwise SwiftUI shrink-wraps theHStackto its buttons and the row looks narrower than the panel.- 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.
When you can skip nookContentInsets
Section titled “When you can skip nookContentInsets”- 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.
Anti-pattern: double horizontal padding
Section titled “Anti-pattern: double horizontal padding”// 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.
Clearing the notch
Section titled “Clearing the notch”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.
Views beside the notch
Section titled “Views beside the notch”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 = falseconfiguration.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.
Laying out around the notch yourself
Section titled “Laying out around the notch yourself”@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:).
See also
Section titled “See also”Examples/LayoutNook/main.swift- working 600 pt host with trimmed bottom inset and a full-width command row.- Theming - corner radii and
expandedContentInsetsin 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.