Live activities
The compact row beside the notch, and the detail behind it.
A live activity is Droppy's version of the Dynamic Island: a compact status that stays visible while something is happening, in the wings either side of the notch — or inside a floating island on a display without one.
Only one droplet holds the compact seat at a time. You publish a state; the host decides who wins.
Publishing state
liveActivityState is a publisher. Emit a LiveActivityState while you have something to say, and nil to stand down.
extension WorldClockDroplet: LiveActivityProviding {
public var liveActivityState: AnyPublisher<LiveActivityState?, Never> {
activitySubject.eraseToAnyPublisher()
}
private func publishActivity() {
activitySubject.send(
LiveActivityState(
priority: 200,
accessibilityTitle: "Amsterdam \(time)",
isInteractive: false
)
)
}
}Emitting every second is fine — the host diffs before it redraws. What is not fine is holding the seat when you have nothing to show: publish nil the moment your task ends.
priority decides who wins the seat. Keep it honest: a timer that is counting down outranks a status that merely exists, and a droplet that pins itself high is one users turn off.
The compact presentation
Two builders, one per wing:
public func makeCompactLeading() -> AnyView {
AnyView(
Image(systemName: "globe")
.font(.system(size: DroppyLiveActivityMetrics.iconSize, weight: .medium))
.foregroundStyle(AdaptiveColors.notchSurfacePrimaryText)
.padding(.trailing, DroppySpacing.sm)
)
}
public func makeCompactTrailing() -> AnyView {
AnyView(
Text(time)
.font(.system(size: DroppyLiveActivityMetrics.labelFontSize, weight: .medium, design: .rounded))
.monospacedDigit()
.foregroundStyle(AdaptiveColors.notchSurfacePrimaryText)
.padding(.leading, DroppySpacing.sm)
)
}The space is small and fixed: 70pt per wing at rest, 120pt when the row is interactive. Use monospacedDigit() on anything that ticks, or the row jitters as digits change width.
Expanded
makeExpanded(context:) is the detail behind the compact row, shown when the user opens the shelf. It gets the full width in context.availableWidth.
Seats
seat reports what you currently hold:
compact— the primary seat.secondary— the companion seat, when the host is showing a pair.none(_:)— with a reason:outranked,userDisabled,surfaceSuppressed,pairFull, and others.
liveActivitySeatDidChange(_:) tells you when it moves. Use it to stop work nobody can see. Losing the seat is normal, not an error.
To stand down before your publisher catches up:
host.liveActivity.yield(reason: .idle)Interactive rows
Set isInteractive on the state when your row has controls. The host widens the wings, and DroppyLiveActivityControlStyle gives you the 24pt circular control Droppy's own media row uses.
Button { skip() } label: { Image(systemName: "forward.fill") }
.buttonStyle(DroppyLiveActivityControlStyle(prominence: .accent))The companion pair
When Droppy shows two activities side by side, the second renders through makeCompanionCompact(context:) with the slot size in context.slotSize. Return a narrower composition; the default reuses your trailing wing, which is usually too wide.