ExpandedSurfaceProviding
A provider of expanded surfaces: content, size, and the far end of the host's lifecycle.
@MainActor protocol ExpandedSurfaceProviding : AnyObject
Deliberately AnyObject rather than Droplet, the same split QuickActionBarProviding makes and for the same reason: the object that knows whether a session is running is usually a long-lived manager the droplet owns, not the droplet itself. Vend one by adopting ExpandedSurfaceHosting.
The split matters more here than it does for the action bar. The host resolves this provider through the droplet registry, so there is exactly one instance and both sides agree on which. The shipping code this replaces used a shared singleton reachable from anywhere in the app, which is why eight unrelated host sites ended up reading it directly and why none of that could survive the move out of the host process boundary.
When the host reads the surface
expandedSurfacesis read each time the droplet callspresentExpandedSurface(_:), to find the descriptor for the requested id. A presentation keeps the descriptor it was presented with until it is dismissed, itssuppressesabove all: droppingautoCollapsehalfway through would close the shelf under the user, and addingoutsideClickDismissalwould take away the way out they were using. A changed descriptor applies on the next presentation.expandedSurfaceSize(_:fitting:)is asked on the shelf's layout passes, and again while the surface is up whenever the droplet broadcasts a state change: itsobjectWillChangefires (when the droplet is anObservableObject); one of its keys inDropletPreferencesServicechanges; an@Observableproperty read during the previous call changes (when the droplet is not anObservableObject); orstatePublisherfires. That second ask repeats the shelf's last proposal, and the shelf re-lays itself out only when the answer changed. Broadcasts are coalesced: a burst costs one call, at most about four a second.expandedSurfaceProvideris resolved on every render pass, and after a broadcast while the surface is up: when it names another object, or none, the shelf draws from the provider it returns now, an empty view when there is none. A broadcast never dismisses a presented surface, and one you stop offering stays up until it is dismissed. CalldismissExpandedSurface(_:)before you take its provider away.
The host hears the droplet object, not the provider. A provider that is a separate object (a session the droplet owns) reaches the host only by forwarding its objectWillChange to the droplet's own, or by storing the value in preferences; @Observable state is heard wherever it lives, but only for a droplet that is not itself an ObservableObject. Droppy 16.0.1 and earlier asked the size only on layout passes the shelf made for its own reasons, so a surface that grew stayed at its old size until something else moved the shelf.
- Note: Maturity: Stable. Droppy mounts this: your surface replaces the shelf's own contents, sized from your
expandedSurfaceSize(_:fitting:)proposal and clamped by the host, and every way out of it callsexpandedSurfaceDidDismiss(_:presentation:reason:)with the real reason. Requires theexpanded-surfacecapability. See Host support.
Properties
expandedSurfaces
@MainActor var expandedSurfaces: [ExpandedSurfaceDescriptor] { get }
The surfaces this droplet can present. Read each time the droplet presents one (see above), never on a timer; keep it stable and cheap.
Methods
expandedSurfaceDidDismiss(_:presentation:reason:)
@MainActor func expandedSurfaceDidDismiss(_ id: ExpandedSurfaceID, presentation: ExpandedSurfacePresentation, reason: ExpandedSurfaceDismissalReason)
The host has taken the surface down.
Called once per presentation, after the close, for every reason including your own dismissExpandedSurface(_:). Compare presentation against the one you started, because a delayed teardown can arrive after the user has already summoned the surface again.
expandedSurfaceDidDismiss(_:presentation:reason:)
@MainActor func expandedSurfaceDidDismiss(_ id: ExpandedSurfaceID, presentation: ExpandedSurfacePresentation, reason: ExpandedSurfaceDismissalReason)
Default: nothing to tear down.
expandedSurfaceSize(_:fitting:)
@MainActor func expandedSurfaceSize(_ id: ExpandedSurfaceID, fitting proposal: ExpandedSurfaceSizeProposal) -> CGSize?
How big the surface wants to be, or nil for the host's standard size.
Called on layout passes rather than once per presentation, because a surface whose content grows (a transcript arriving a word at a time, a list gaining rows) has to be able to change its answer, and asked again after each state change the droplet broadcasts while the surface is up (see above), which is how a new answer reaches the shelf. Keep it pure and cheap: no side effects, no allocation you can avoid, and no animation of your own. The host clamps to maximumSize and holds your answer still across its springs.
expandedSurfaceSize(_:fitting:)
@MainActor func expandedSurfaceSize(_ id: ExpandedSurfaceID, fitting proposal: ExpandedSurfaceSizeProposal) -> CGSize?
Default: no preference, so the host uses its standard expanded size.
makeExpandedSurfaceView(_:context:)
@MainActor func makeExpandedSurfaceView(_ id: ExpandedSurfaceID, context: ExpandedSurfaceContext) -> AnyView
Builds the surface's live view. Views render on dark shelf surfaces; use light foreground colors and the DroppyKit design tokens.
Return an empty view for an id you do not recognize rather than trapping. A host can ask about a surface you published a moment ago and have since stopped offering.