Article
Your own panel
Put any SwiftUI view in the pill.
Overview
struct MonitorPanel: View {
@Bindable var model: Monitor // your own model: the panel is rebuilt on every open
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
NotchTabBar(Monitor.Tab.allCases, selection: $model.tab) { $0.title }
NotchMenuButton(menu: model.menu) { _ in Image(systemName: "ellipsis.circle") }
}
NotchLiveList(model.feed) { FeedRow(item: $0).notchLiveAction() }
}
.padding(16)
}
}content.expanded = NotchBody(height: 220) { MonitorPanel(model: model) }Tabs, a menu and a live list in the hover panel. Each piece has a recipe in Recipes.
| Body | Shows | Clicks |
|---|---|---|
showing |
below the notch while status is showing or attention |
no: hovering opens expanded |
expanded |
while the pointer is on the pill, or a menu from it is open | yes |
prompt |
a question: in the pill at a notch, else in a floating window | yes |
Size
content.expanded = NotchBody(width: 440, height: 200) { HoverPanel() }
content.showing = NotchBody(height: { placement in placement == .menuBar ? 40 : CGFloat(captionLines()) * 20 + 20 }) {
Text("Captions")
}heightis below the band: a number, or a closure of the placement that may read your settings. It's read inside observation, so the pill resizes when what it reads changes.widthis optional: 500 showing, 560 expanded, 540 asking by default.bottomRadiusshapes ashowingbody only.- The view is centred in its height; frame it with
alignment: .topto sit at the top. - Give a panel a fixed height: content that changes height makes the pill jump.
- The panel window caps the pill: 680 × 420 pt alone, 1000 × 520 pt docked. Keep an expanded body under about 300 pt.
What a body can read
@Environment(NotchViewModel.self) private var pill:
| Property | What it is |
|---|---|
placement |
.notch, .belowNotch (another app has the notch) or .menuBar (no notch) |
isDark |
black pill at a notch, rather than glass below the menu bar |
mode |
hidden, quiet, showing, expanded, asking |
bodyTextWidth |
the text width for a showing body, steady while the pill animates |
dockedBodyWidth |
the width a docked part gives you, nil in your own pill |
theme, metrics, geometry |
colours and fonts, fixed sizes, the screen and notch |
settings |
the notch settings (settings.notch.fontSize is the user's text size) |
content, level, needsAttention |
your content, the meter level, status == .attention |
isHovering, isMenuOpen |
the pointer is on the pill; a menu from it is open |
slot, showsSwitcherDots, switcherRowHeight |
your share of a docked notch, and the Switcher dots' row |
Inside the pill the colour scheme is dark (glass follows the system), so .primary and .secondary read.
Rules
- State lives in your model. A body exists only while its mode shows;
@Statelasts one opening. - Menus:
NotchMenuButtononly. A SwiftUIMenucan't tell the pill it's open, and the pill closed under it. - No keyboard focus. The pill never becomes key: no text fields, no shortcuts. Never give an answer a shortcut.
- Clicks reach the pill only while it's hovered and only over the pill as drawn; clicks beside it go through.
- Lists that change use
NotchLiveList(Live lists).
Questions in the floating window
struct ConnectionQuestion: View {
@Environment(NotchViewModel.self) private var pill: NotchViewModel? // nil in the floating window
var body: some View {
Text("Allow this connection?")
.frame(width: pill?.bodyTextWidth ?? 300)
}
}The floating window (NotchPromptWindowController) puts no NotchViewModel in the environment: a prompt body that
reads it must read it as optional.
When apps share the notch
- Split: your
showingbody may get part of the width. UsebodyTextWidth, which is that part's width. - Merged: hovering widens the whole stack, so an
expandedbody may be wider than itswidth. Let it stretch. - Switcher: with two or more apps docked, the open pill shows a dot per app in a 26 pt row above your body, centred under the notch. The package makes the room; your body keeps its height below the row.