Expanded surfaces
A takeover on the notch, for something that needs the whole shelf.
An expanded surface replaces the shelf's normal contents with yours: a picker, a form, a full view of something a widget can only summarise. It is the biggest thing a droplet can put on screen, and the one that most needs restraint — while it is up, the user cannot see anything else Droppy would have shown.
Requires the expanded-surface capability.
Declaring one
extension WorldClockDroplet: ExpandedSurfaceProviding {
public var expandedSurfaces: [ExpandedSurfaceDescriptor] {
[
ExpandedSurfaceDescriptor(
id: "clocks-detail",
title: "World Clock",
systemImage: "globe",
suppresses: [.shelfWidgets, .autoCollapse]
)
]
}
}What it suppresses
ExpandedSurfaceHostSuppression is the host chrome your surface turns off while it is presented. Ask for the least you need:
| Option | Turns off |
|---|---|
shelfWidgets | The normal widget row |
favoritesBar | The favourites strip |
floatingNavLane | The floating navigation lane |
notificationBanners | Banners over the shelf |
autoCollapse | The shelf closing on its own |
outsideClickDismissal | Clicking away closing it |
swipeGestures | Swipe-to-close |
takeover is the full set. Reach for it only when a stray click really would lose the user's work: suppressing outsideClickDismissal means the obvious way out stops working, and a surface that traps someone is worse than one that closes early.
Sizing
public func expandedSurfaceSize(
_ id: ExpandedSurfaceID,
fitting proposal: ExpandedSurfaceSizeProposal
) -> CGSize? {
CGSize(width: proposal.standardSize.width, height: CGFloat(clocks.count) * 44 + 64)
}Return nil to take proposal.standardSize. The proposal also carries maximumSize, the surface kind, and the display, so a surface can be shorter on a small screen.
The shelf asks on its layout passes, and again while your surface is up each time the droplet broadcasts a state change: its objectWillChange fires (when the droplet is an ObservableObject), one of its keys in DropletPreferencesService changes, an @Observable property read during the previous call changes (when it is not), or statePublisher fires. That ask repeats the shelf's last proposal, and the shelf re-lays itself out only when your answer changed, so a surface that grows (the clock list above gaining a row) resizes the shelf without you doing anything else. A burst of broadcasts costs one call, at most about four a second.
Droppy hears the droplet object, not the provider. When the provider is a separate object, a session the droplet owns, forward its objectWillChange to the droplet's own, or store 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 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.
Changing a surface while it is up
expandedSurfaces is read when you present, to find the descriptor for the id you asked for. A presentation keeps that descriptor until it is dismissed, its suppressions above all: dropping autoCollapse halfway through would close the shelf under the user, and adding outsideClickDismissal would take away the way out they were using. A changed descriptor applies on the next presentation.
A broadcast never dismisses a presented surface. If expandedSurfaceProvider starts returning another object, the shelf draws from that one; if it returns nil, the shelf draws an empty surface until the user closes it. When a session ends, call dismissExpandedSurface(_:) before you take its provider away.
Every size here is the content area: what your view draws in, and what availableSize hands you back, clamped. The host adds the shelf's own chrome around it (the side insets, the cutout clearance, the slot gutter), so a 400-point-tall answer is a 400-point-tall surface, not a 400-point-tall shell with your bottom row behind the shell's edge. The standard size is a solo widget card in the shelf's minimum shell (352 × 93 on a notch, 370 × 93 as an island); the ceiling is two thirds of the display less the chrome.
Presenting and dismissing
let presentation = host.notchSurface.presentExpandedSurface(
ExpandedSurfacePresentationRequest(surfaceID: "clocks-detail", opensShelf: true)
)ExpandedSurfacePresentation identifies the presentation, not the surface, and the difference matters. Tearing a takeover down runs after the close spring settles, and by then the user may have summoned the same surface again — a teardown keyed on the surface id would kill the fresh one. Capture the presentation you started with and compare before acting on a delayed callback.
expandedSurfaceDidDismiss(_:presentation:reason:) is called once per presentation, for every reason including your own dismissal. ExpandedSurfaceDismissalReason tells you which: the user collapsed the shelf, another surface pre-empted yours, the display changed, the shelf was turned off, or host policy ended it.