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 new worldclock --name "World Clock"
cd worldclockdroppykit new writes the package, a starter Icon Composer document, a placeholder creator avatar, droplet.json, and the harness target. If you would rather start from Examples/WorldClock, copy that directory instead.
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://github.com/jane/worldclock-droplet",
"commit": "9f1c2ab5c0e4d3f6a7b8c9d0e1f2a3b4c5d6e7f8",
"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 runThe 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 submission runs7. Submit
Push the repository, then submit its URL and the exact commit at getdroppy.app/submit-droplet. See Submitting.