Your first droplet

Scaffold a droplet, run it in the harness, and see it on the shelf.

This walks through a droplet that shows the time in another timezone on Droppy's shelf. It is the shortest thing that is still a real droplet: an entry point, a surface, and a manifest.

The finished version ships with the SDK as Examples/WorldClock, including its Icon Composer document and its harness target.

1. Scaffold

droppykit is the SDK repository's Scripts/droppykit, on your PATH as Add the package sets up.

droppykit new worldclock --name "World Clock"
cd worldclock

droppykit new writes the package, a placeholder Icon Composer document (leave it: Jordy designs every Store droplet's icon, see The icon), a placeholder creator avatar, droplet.json, a CHANGELOG.md and a README.md to fill in, and the harness target. The avatar and the words are placeholders that build and validate; the command's output lists what to replace before you submit. If you would rather start from Examples/WorldClock, copy that directory instead.

It also writes AGENTS.md, CLAUDE.md and the MCP wiring for Claude Code, Codex and Cursor. If a coding agent is going to write most of this droplet, open the folder in it now and let it read the brief; see Build with a coding agent. The rest of this walkthrough is the same either way.

2. The entry point

Every droplet has two types. The principal is what Droppy's loader instantiates through NSPrincipalClass; it is @objc and does nothing but make the droplet. The droplet is the object that lives for the session.

import DroppyKit
import SwiftUI

@objc(WorldClockPrincipal)
public final class WorldClockPrincipal: NSObject, DropletPrincipal {
    public override init() { super.init() }
    @MainActor public func makeDroplet() -> AnyObject { WorldClockDroplet() }
}

@MainActor
public final class WorldClockDroplet: NSObject, ObservableObject, Droplet {
    public nonisolated static let id: DropletID = "worldclock"

    private var host: DropletHost?
    @Published private var now = Date()
    private var ticker: AnyCancellable?

    public func activate(host: DropletHost) throws {
        self.host = host
        ticker = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] date in self?.now = date }
    }

    public func deactivate() {
        ticker?.cancel()
        ticker = nil
        host = nil
    }
}

Important. Everything activate(host:) starts must be torn down in deactivate(). Swift cannot unload code, so a droplet that leaves a timer running keeps running until Droppy relaunches.

3. A surface

Conform to ShelfWidgetProviding, declare the widget, and build its view.

extension WorldClockDroplet: ShelfWidgetProviding {
    public var widgetDescriptors: [ShelfWidgetDescriptor] {
        [
            ShelfWidgetDescriptor(
                id: "clocks",
                title: "World Clock",
                systemImage: "globe",
                layoutTraits: ShelfWidgetLayoutTraits(
                    preferredSoloWidth: 420,
                    preferredPairedWidth: 210,
                    contentHeight: .fixed(150)
                )
            )
        ]
    }

    public func makeWidgetView(_ id: ShelfWidgetID, context: ShelfWidgetContext) -> AnyView {
        AnyView(WorldClockWidget(droplet: self, context: context))
    }

    public func makeWidgetSettingsPopover(_ id: ShelfWidgetID) -> AnyView? { nil }
}

Both widths are required. Droppy refuses a descriptor that leaves either to a host fallback, because the shelf's layout is one algorithm over supplied numbers and a missing number has no correct default.

4. The manifest

droplet.json describes the droplet to everything that is not the droplet: the build script, the validator, the Store, and getdroppy.app.

{
  "id": "worldclock",
  "name": "World Clock",
  "version": "1.0.0",
  "summary": "Every timezone you care about, on the shelf",
  "category": "Productivity",
  "kit": { "abi": 1, "minAPI": "1.0.0" },
  "minAppVersion": "15.3.0",
  "capabilities": ["shelf-read"],
  "surfaces": ["shelf-widget"],
  "icon": "WorldClock.icon",
  "creator": {
    "name": "Jane Doe",
    "kind": "community",
    "url": "https://jane.example",
    "avatar": "Creator.png"
  },
  "source": {
    "repository": "https://gitlab.com/droppyformac1/droplets/-/tree/main/droplets/worldclock",
    "license": "MIT"
  }
}

surfaces and your conformances must agree. The harness reports the difference, because the two disagreeing is the most common reason a droplet validates and then does nothing.

5. Run it

droppykit run

The harness opens on your droplet's Overview page. Switch to Shelf widgets to see it solo and paired, side by side.

6. Build and validate

droppykit build      # produces WorldClock.droplet
droppykit validate   # the same checks the Store repository runs
droppykit submit     # fork the Store repository, push a branch, open the merge request

7. Submit

The Store is a repository, gitlab.com/droppyformac1/droplets, one folder per droplet. droppykit submit opens the merge request that adds yours; when it merges, the pipeline builds, signs and publishes it, and every Mac running Droppy can install it within minutes. Replace the scaffold's placeholders first, all but the icon, and read Good citizen for what review checks beyond the build. See Submitting.

See also