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.

See also