The manifest
The lifecycle, the host, and the capabilities a droplet runs under.
A droplet is created once per Droppy launch and activated when it is installed, enabled, and not removed. It runs in-process, on the main actor, and shares fate with the app.
That is why the Store is review-and-sign rather than open loading: a droplet has Droppy's permissions, so a droplet is read before it is published.
Lifecycle
activate(host:) is called once, after the host's own managers are up. Store the host — there is no global accessor, and if you did not keep it you cannot reach it. Keep activation fast; the host runs it under a watchdog.
Throwing marks the droplet failed for the session. The user sees it as unavailable on its Store page, with the error.
deactivate() is called when the user disables or removes the droplet. Tear down everything: timers, observers, windows, registered shortcuts. After it returns the droplet must be inert.
prepareForRemoval() is a veto. Return false to cancel a removal that would leave something in a bad state — a transfer in flight, an unsaved document. It defaults to true.
The host
DropletHost is the whole surface. Every service is present whether or not you may use it; the effect is gated. A call you lack the capability for returns false or nil and is logged, so a missing grant never crashes.
func activate(host: DropletHost) throws {
self.host = host
if host.isGranted(.globalShortcuts) {
host.shortcuts.register(id: "toggle", title: "Toggle World Clock", defaultShortcut: nil) {
// …
}
}
}Check isGranted(_:) to hide an affordance you cannot honour, not as a substitute for handling refusal — capabilities can be revoked while you run.
Capabilities
Declare what you need in droplet.json. The user sees the list before installing, so ask for what you use and nothing more. A long list is the single most common reason a submission is sent back.
| Capability | Grants |
|---|---|
hud | Presenting transient HUDs on the notch |
shelf-read | Observing the shelf's state |
shelf-write | Opening and closing the shelf |
expanded-surface | Presenting takeover surfaces |
global-shortcuts | Registering shortcuts the user can rebind |
clipboard-read / clipboard-write | Reading and writing the pasteboard |
network-client | Outbound network requests |
menu-bar | A menu bar extra |
lock-screen | A row on the lock surface |
The full list is DropletPermission.
Storage
DropletPreferencesService namespaces every key to droplet.<your-id>.<key>, so two droplets can both store "interval" without colliding and neither can read the other's. That namespacing is why DropletID forbids . — without the rule, droplet a writing b.c and droplet a.b writing c are the same key.
Settings survive disable, re-enable, update, and removal. A user who removes and reinstalls finds their configuration intact.
For files, containerDirectory is a private per-droplet directory that persists across updates.
Logging
Use DropletLogService, never print. A droplet runs inside an installed app, and print from there goes nowhere a user or a reviewer can find.
host.log.info("activated for \(zone)")