Live Activities

Present current state in DynamicLake's compact area.

Overview

A DynamicLake live activity is compact content that appears around the notch or pill. Use it for active, current information that people can understand at a glance.

DynamicLake provides the panel, clipping, notch gap, and screen-aware geometry. Your extension or plugin provides the content inside the left and right slots.

Extensions render live activities with SwiftUI. Plugins describe live activities with JSON components. Use JSONPluginAPI when you are building a plugin.

Best Practices

Create the Scene

Add a DynamicLakeLiveActivity scene to the ExtensionKit target.

import DynamicLakeKit
import SwiftUI

@main
struct ClockDynamicLakeExtension: DynamicLakeExtension {
    var body: some DynamicLakeExtensionScene {
        DynamicLakeLiveActivity {
            ClockLiveActivityView()
        }
    }
}

Keep the scene focused on compact content. Start, update, and close the activity from the companion app.

Build the Layout

Use LiveActivitySlotLayout for the compact left and right regions. The center is reserved for the notch or pill shape.

import DynamicLakeKit
import SwiftUI

struct ClockLiveActivityView: View {
    let geometry: LiveActivityGeometry?

    init(geometry: LiveActivityGeometry? = nil) {
        self.geometry = geometry
    }

    var body: some View {
        GeometryReader { proxy in
            let geometry = resolvedGeometry(for: proxy.size)

            LiveActivitySlotLayout(geometry: geometry) {
                Image(systemName: "clock.fill")
                    .font(.system(size: geometry.compactSymbolFontSize, weight: .semibold))
                    .foregroundStyle(.white)
                    .liveActivityLeftSlotSymbol(
                        in: geometry,
                        visualWidth: geometry.compactSymbolVisualWidth
                    )
            } right: {
                LiveActivityCompactTextSlot(
                    text: "12:45",
                    geometry: geometry,
                    fontSize: geometry.compactProminentTextFontSize,
                    weight: .semibold,
                    foregroundStyle: AnyShapeStyle(.white)
                )
            }
            .frame(width: proxy.size.width, height: proxy.size.height)
        }
    }

    private func resolvedGeometry(for size: CGSize) -> LiveActivityGeometry {
        if let geometry {
            return geometry
        }

        return LiveActivityGeometry.currentOnActiveScreen(
            totalSize: CGSize(
                width: max(size.width, 1),
                height: max(size.height, 1)
            )
        )
    }
}

Use liveActivityLeftSlotSymbol(in:visualWidth:) for leading icons or artwork. Use liveActivityRightSlotAccessory(in:visualWidth:) for trailing status or progress, and reserve liveActivityRightSlotText(in:) or LiveActivityCompactTextSlot for duration-style text such as time or timers.

In provider-based extension views, prefer context.geometry over LiveActivityGeometry.currentOnActiveScreen(...) so hosted agents use the same left-slot spacing as DynamicLake's built-in activities. The context overloads also apply configuration padding automatically:

let geometry = context.geometry

LiveActivitySlotLayout(context: context) {
    Image(systemName: "clock.fill")
        .font(.system(size: geometry.compactSymbolFontSize, weight: .semibold))
        .foregroundStyle(.white)
        .liveActivityLeftSlotSymbol(in: context)
} right: {
    LiveActivityCompactTextSlot(
        text: "12:45",
        context: context,
        weight: .semibold
    )
}

Avoid hard-coding the total width, notch gap, or side-slot padding. Pill-shaped displays use a narrower compact panel and a wider sneak-peek panel than the default notch layout, so fixed widths can look correct in one shape and wrong in the other.

DynamicLake owns the outer panel size and pill content scale for installed extension scenes. Keep your compact view sized to the frame it receives; do not apply another scale to the whole live activity view.

JSON plugins use the same compact activity model with predefined leftSlot and rightSlot components. See JSONPluginAPI for the wire format.

Match Built-In Spacing

To match built-in activities such as Calendar, keep the configured LiveActivitySize and the rendered geometry on the same path. Use the geometry from the provider context, use compactSymbolVisualWidth for the left symbol frame, use the right-slot accessory helper for status or progress, and reserve LiveActivityCompactTextSlot for duration-style text.

struct TaskActivityProvider: DynamicLakeLiveActivityViewProvider {
    typealias State = TaskActivityState

    let configuration = DynamicLakeLiveActivityConfiguration(
        id: "task",
        priority: .normal,
        size: .large,
        supportsSneakPeek: true,
        rightAccessoryExtraTrailingPadding: 4
    )

    func compactView(context: DynamicLakeLiveActivityContext<State>) -> some View {
        let geometry = context.geometry

        LiveActivitySlotLayout(context: context) {
            Image(systemName: context.state.symbolName)
                .font(.system(size: geometry.compactSymbolFontSize, weight: .semibold))
                .foregroundStyle(context.state.tint)
                .frame(
                    width: geometry.compactSymbolVisualWidth,
                    height: geometry.compactSymbolVisualWidth
                )
                .liveActivityLeftSlotSymbol(in: context)
        } right: {
            LiveActivityCompactTextSlot(
                text: context.state.durationText,
                context: context,
                weight: .semibold
            )
        }
    }

    func sneakPeekView(context: DynamicLakeSneakPeekContext<State>) -> some View {
        SneakPeekSlotLayout(
            geometry: context.geometry,
            leftSlotSizing: .fixed(SneakPeekControlMetrics.slotSize),
            rightSlotSizing: .empty,
            sideSpacing: 8,
            centerAlignment: .center
        ) {
            Image(systemName: context.state.symbolName)
                .font(.system(size: SneakPeekControlMetrics.symbolSize, weight: .bold))
                .foregroundStyle(context.state.tint)
                .frame(
                    width: SneakPeekControlMetrics.circleSize,
                    height: SneakPeekControlMetrics.circleSize
                )
        } center: {
            Text(context.state.detailText)
                .font(.system(size: 12, weight: .semibold))
                .lineLimit(1)
                .minimumScaleFactor(0.75)
                .foregroundStyle(context.textColor)
        } right: {
            SneakPeekSlot.empty
        }
    }
}

When a view is not rendered from a provider context, resolve fallback geometry from the actual frame that DynamicLake gives the extension:

LiveActivityGeometry.currentOnActiveScreen(
    size: .large,
    totalSize: CGSize(
        width: max(proxy.size.width, 1),
        height: max(proxy.size.height, 1)
    ),
    enforcesMinimumSideSlots: !LiveActivityScreenKind.current.isNonNotch
)

This keeps third-party compact views and sneak peeks aligned with the same measured notch gap, side-slot width, and symbol scale used by DynamicLake's built-in activities.

When your view is hosted by DynamicLake, prefer the provider context instead of rebuilding this fallback yourself. The host adjusts the geometry for active shape changes, including pill compact scaling and sneak-peek widening.

!Left and right live activity slots aligned around the center notch gap.

Choose a Size

Choose the size that fits the compact content clearly without adding labels. Use LiveActivitySize/large when the visual, status, progress, or duration layout needs the widest compact live activity.

DynamicLakeLiveActivityConfiguration(
    id: "clock",
    priority: .normal,
    size: .large,
    supportsSneakPeek: false
)

| Size | Baseline size | Use for | | --- | --- | --- | | .small | 230 x 31 | Icon-only, status, progress, or timer layouts that must stay tight. | | .normal | 240 x 31 | Visual, status, progress, or duration layouts that need slightly more room than .small. | | .large | 320 x 31 | Widest compact activity for visual, status, progress, or duration layouts that need the most room. |

The selected size controls the geometry rules, side-slot width, and center gap. It is not only a visual width preset. The baseline values above are fallbacks; on a real screen DynamicLakeKit derives the final width, height, radius, and notch gap from the active display.

!Small, normal, and large live activity sizes compared side by side.

Choose a Priority

Use LiveActivityPriority to tell DynamicLake which active activity should be preferred when multiple activities are available.

| Priority | Use for | | --- | --- | | .low | Long-running, ambient activities that can wait behind more immediate content, such as calendar events and ongoing status. | | .normal | The default for active work users may want to glance at, such as music playback, file uploads, builds, and downloads. | | .high | Important, time-sensitive activity the user should not miss, such as calls, messages, and urgent notifications. |

Use .normal unless your activity clearly fits the low- or high-priority cases. Do not use .high just to make an activity more visible.

Priority does not change layout. Use size for layout and priority for ordering.

Preview the Activity

Create a provider for simulator previews.

import DynamicLakeKit
import SwiftUI

struct ClockLiveActivityProvider: DynamicLakeLiveActivityViewProvider {
    typealias State = DynamicLakeEmptyLiveActivityState

    let configuration = DynamicLakeLiveActivityConfiguration(
        id: "clock",
        priority: .normal,
        size: .large,
        supportsSneakPeek: false
    )

    func compactView(context: DynamicLakeLiveActivityContext<State>) -> some View {
        ClockLiveActivityView(geometry: context.geometry)
    }

    func sneakPeekView(context: DynamicLakeSneakPeekContext<State>) -> some View {
        EmptyView()
    }
}

Show the compact presentation in a regular macOS app target.

DynamicLakeLiveActivitySimulator(
    provider: ClockLiveActivityProvider(),
    mode: .compact,
    panelShape: .notch
)

Test both .notch and .pill.

Update State

Use DynamicLakeActivityCenter from the companion app to open, update, and close an activity.

try DynamicLakeActivityCenter.setActivityActive(
    true,
    extensionBundleIdentifier: "com.example.ClockDynamicLakeExtension",
    title: "Clock",
    supportsSneakPeek: false,
    size: .large,
    state: ClockActivityState(timeText: "12:45")
)

See ActivityState for the full state flow.

Topics

Configuration

Layout