Shelf widgets
Cards on the expanded shelf, in both the solo and paired layouts.
The shelf is Droppy's expanded surface below the notch. A widget is a card on it. The shelf owns one layout algorithm and widgets supply parameters to it, so the numbers you give in ShelfWidgetLayoutTraits are what the shelf lays out with — there is no per-widget special case in the host.
Declaring a widget
extension WorldClockDroplet: ShelfWidgetProviding {
public var widgetDescriptors: [ShelfWidgetDescriptor] {
[
ShelfWidgetDescriptor(
id: "clocks",
title: "World Clock",
systemImage: "globe",
layoutTraits: ShelfWidgetLayoutTraits(
preferredSoloWidth: 420,
preferredPairedWidth: 210,
contentHeight: .fixed(150)
),
searchKeywords: ["time", "timezone"]
)
]
}
}widgetDescriptors is read at activation and again after every host state-change broadcast, so keep it stable and cheap.
Important. preferredSoloWidth and preferredPairedWidth are both required. They are typed as optional for binary compatibility only; a current host refuses a descriptor that omits either. nil does not select a fallback.
Solo and paired
Solo — the widget is alone on the shelf and gets the wide standalone canvas. placement is .solo, isPaired and isCompact are false.
Paired — two to four widgets share a row. Each gets its settled slot width, isPaired and isCompact are true, and placement is .leading, .middle, or .trailing.
These are different compositions. Solo has room for a row per item; paired has room for one number. Branch on context.isCompact:
var body: some View {
VStack(alignment: .leading, spacing: DroppySpacing.sm) {
header
if context.isCompact { compactBody } else { fullBody }
Spacer(minLength: 0)
}
.padding(DroppySpacing.mdl)
.frame(maxWidth: .infinity, alignment: .leading)
}Never branch on a width comparison. The same width means different things on a notch and on an island, and a threshold that works today breaks when a widget is added beside yours.
Height
ShelfWidgetContentHeight is .standard or .fixed(_:). Pick .fixed when your content has a natural height; the shelf clamps unreasonable values.
For content-adaptive sizing — height that depends on the data, not just the type — report the size from the view and tell the host to re-measure:
host.shelf.invalidateLayout(for: "clocks")Call it when your model crosses a size threshold, not on every keystroke. A widget that re-measures continuously is a shelf that never settles.
Focus
keyboardFocusable tells the shelf your widget's primary surface is a text field the user types into the instant it opens. The shelf then pulls keyboard focus when the widget opens alone, so the cursor lands without an extra click. Non-typing widgets never steal focus.
Prewarming
never opts out of being built before it is shown. Use it when construction is expensive or has side effects; the default is to allow prewarming, which makes the shelf open feel instant.
Both notch shapes
context.usesIslandCurvature is true when the shelf is drawn as a floating island rather than hanging from a hardware notch. context.usesAdaptiveForegrounds asks you to let the host tint your foregrounds.