Droplet

The runtime object representing one droplet inside the host process.

@MainActor protocol Droplet : AnyObject

A droplet is created once per launch (via makeDroplet()) and activated when it is installed, enabled, and not removed. Capabilities beyond the base lifecycle are discovered by conditional cast to the optional capability protocols (ShelfWidgetProviding, SettingsPaneProviding, LiveActivityProviding, and the rest).

Droplets run in-process on the main actor and share fate with the host app. There is no sandbox between a droplet and Droppy; the store is review-and-sign precisely because of that. Keep activate fast (the host runs it under a watchdog deadline) and never block the main thread.

The protocol is @MainActor, which states in the type system what the paragraph above states in prose. Every capability protocol refines Droplet and shares that isolation, so a droplet builds its model in activate(host:) on the same actor its view factories run on and needs no MainActor.assumeIsolated anywhere. Before DroppyKit 0.3.0 the lifecycle was nonisolated while the view factories were not, and every droplet paid for the mismatch with an escape hatch that did not compile in Swift 6 language mode.

Since 0.4.0 every capability protocol spells @MainActor explicitly rather than relying on inference from the refinement. The isolation is identical either way inside this module; the difference is at the module boundary. This module compiles in Swift 5 language mode, and a Swift 5 module does not export inference-derived protocol isolation, so a droplet compiled with -swift-version 6 saw the refining protocols' requirements as nonisolated and its own main-actor witnesses were rejected as conformance-isolation errors. The explicit attribute is serialized and printed in the generated interface, which makes every capability protocol adoptable from Swift 6 language mode.

Type properties

id

nonisolated static var id: DropletID { get }

Stable identifier. Must equal the DroppyDropletID Info.plist key and the store manifest id.

Nonisolated so the loader can read it during the identity cross-check before the droplet is fully constructed, and from whatever context the bundle boundary hands it back on.

Properties

id

nonisolated var id: DropletID { get }

Instance-side convenience for the static identifier. Nonisolated, matching id-swift.type.property.

Methods

activate(host:)

@MainActor func activate(host: DropletHost) throws

Called once per launch when the droplet is installed, enabled, and not removed, after the host's own managers are up. Store the host value; it is the only sanctioned way to reach host functionality.

Throwing from activate marks the droplet failed for this session; the host falls back to its built-in twin while one exists.

deactivate()

@MainActor func deactivate()

Called when the user disables or removes the droplet. Tear down timers, observers, windows, and registered shortcuts. Code stays mapped until relaunch (Swift cannot unload); after deactivate the droplet must be inert.

prepareForRemoval()

@MainActor func prepareForRemoval() -> Bool

Veto gate shown before removal, mirroring the existing droplet removal semantics in Droppy. Return false to cancel removal (for example when removal mid-operation would leave the system in a bad state). Defaults to true.

prepareForRemoval()

@MainActor func prepareForRemoval() -> Bool