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.

CapabilityGrants
hudPresenting transient HUDs on the notch
shelf-readObserving the shelf's state
shelf-writeOpening and closing the shelf
expanded-surfacePresenting takeover surfaces
global-shortcutsRegistering shortcuts the user can rebind
clipboard-read / clipboard-writeReading and writing the pasteboard
network-clientOutbound network requests
menu-barA menu bar extra
lock-screenA 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)")

See also