Create Your First Extension

Build, install, and verify a minimal third-party DynamicLake extension.

Overview

A DynamicLake extension has two parts:

The companion app and the extension target both import DynamicLakeKit. The companion app opens, updates, and closes activities with DynamicLakeActivityCenter. The extension target provides DynamicLakeLiveActivity, optional DynamicLakeSneakPeek, optional DynamicLakeExtraLiveActivity, and optional DynamicLakeSettings scenes.

Use this guide when starting from a new Xcode project. Use the demo extension as the reference implementation.

Before designing extension UI, read DesignGuidelines. SwiftUI extensions own their layout, spacing, typography, animation, and assets, so the guidelines matter before implementation.

1. Create The Targets

Create a normal macOS app target first. This is the companion app.

Then add an ExtensionKit extension target. Keep the extension target small: SwiftUI views, simple state models, and required assets only. Put networking, timers, and app logic in the companion app. If you expose settings from the extension, keep that scene as a thin UI over shared preferences or companion-owned state.

Recommended bundle identifiers:

Companion app: com.example.ClockApp
Extension:     com.example.ClockApp.ClockDynamicLakeExtension

The extension bundle identifier is the identifier you pass to DynamicLakeActivityCenter.

2. Choose Container App Dock Behavior

If the companion app exists only to contain the DynamicLake extension, you can make the app an agent app so it does not remain visible as a normal Dock app or show a normal menu bar when macOS launches it for registration, settings, or helper work.

For an extension-only companion app, add this to the companion app target's Info.plist:

<key>LSUIElement</key>
<true/>

In Xcode, this is the Application is agent (UIElement) setting. Set it to YES.

Leave LSUIElement unset or false when the product intentionally has its own main window or should appear in the Dock. DynamicLake does not require LSUIElement and does not block extension loading when it is off. The ExtensionKit .appex still lives inside the containing .app; LSUIElement only changes how that container app behaves after launch.

3. Add DynamicLakeKit

Use Swift Package Manager for new projects. Follow AddSPM, add the DynamicLakeKit product to both targets, then return to this guide.

In the companion app target, add DynamicLakeKit under Frameworks, Libraries, and Embedded Content.

In the extension target, add DynamicLakeKit under Frameworks and Libraries.

If you use DynamicLakeKit.xcframework or DynamicLakeKit.framework instead of SPM:

  1. Link DynamicLakeKit from the companion app target.
  2. Set the companion app target to Embed & Sign.
  3. Link DynamicLakeKit from the extension target.

If the app imports DynamicLakeKit but does not embed it, macOS will crash at launch with a dyld error similar to:

Library not loaded: @rpath/DynamicLakeKit.framework/Versions/A/DynamicLakeKit
Reason: tried: YourApp.app/Contents/Frameworks/DynamicLakeKit.framework ...

That means a manually linked framework is not copied into the app bundle. Set the companion app target to Embed & Sign, rebuild, and reinstall the app.

For a manual universal framework build, verify the framework contains both Intel and Apple Silicon slices:

lipo -info "/path/to/DynamicLakeKit.framework/Versions/A/DynamicLakeKit"

Expected architectures:

x86_64 arm64

4. Set The Extension Point

The extension target's Info.plist must contain the exact DynamicLake extension point identifier.

<key>EXAppExtensionAttributes</key>
<dict>
    <key>EXExtensionPointIdentifier</key>
    <string>com.aviorrok.DynamicLakePro.DynamicLakePro.extension</string>
</dict>

If this string is wrong, the extension can still build and the companion app can still post activity messages, but DynamicLake will not discover the extension UI.

If the extension provides a settings scene, also add:

<key>DynamicLakeSupportsSettings</key>
<true/>

DynamicLake Pro uses that key to decide whether to show the gear button in Extensions settings.

Add optional public metadata keys so DynamicLake settings and Playground can show a useful extension profile:

<key>DynamicLakeExtensionDisplayName</key>
<string>Clock</string>
<key>DynamicLakeExtensionDescription</key>
<string>Shows the current time and upcoming alarms in DynamicLake.</string>
<key>DynamicLakeExtensionDeveloperName</key>
<string>Example Studio</string>
<key>DynamicLakeExtensionWebsiteURL</key>
<string>https://example.com/clock</string>
<key>DynamicLakeExtensionSupportURL</key>
<string>mailto:[email protected]</string>
<key>DynamicLakeExtensionMinimumDynamicLakeKitVersion</key>
<string>1.0.0</string>

All metadata keys are optional. Visible strings can be localized through InfoPlist.strings.

If you add or change extension plist keys after the extension was already built and registered, macOS may keep stale PlugInKit metadata. Fully quit the companion app and DynamicLake Pro, rebuild, and if the updated metadata or gear button still does not appear, force registration again:

pluginkit -a "/path/to/YourApp.app"

5. Bind The Extension Point

In the extension target, create the ExtensionKit entry point.

import DynamicLakeKit
import SwiftUI

@main
struct ClockDynamicLakeExtension: DynamicLakeExtension {
    @available(macOS 26.2, *)
    @AppExtensionPoint.Bind
    var boundExtensionPoint: AppExtensionPoint {
        AppExtensionPoint.Identifier(
            host: DynamicLakeExtensionPoint.hostBundleIdentifier,
            name: DynamicLakeExtensionPoint.name
        )
    }

    var body: some DynamicLakeExtensionScene {
        DynamicLakeLiveActivity {
            ClockLiveActivityView()
                .allowsHitTesting(false)
        }

        DynamicLakeSneakPeek {
            ClockSneakPeekView()
                .allowsHitTesting(false)
        }

        DynamicLakeExtraLiveActivity {
            ClockExtraLiveActivityView()
                .allowsHitTesting(false)
        }

        DynamicLakeSettings {
            ClockSettingsView()
        }
    }
}

Use .allowsHitTesting(false) for passive live activity UI. DynamicLake owns hover, click, right-click, and swipe behavior around the notch. Settings scenes should remain hit-testable because users interact with controls there.

Settings are optional. Add DynamicLakeSettings when users need to configure how the extension behaves inside DynamicLake, such as choosing a display mode, enabling a source, setting a refresh interval, or signing in through your companion app.

DynamicLake Pro hosts DynamicLakeSettings in a fixed 420 x 520 point window. Design the settings view for that size and use internal scrolling when the extension needs more controls. Keep settings UI interactive, but keep live activity, sneak peek, and extra live activity views passive unless they intentionally expose a control.

Keep settings state owned by the companion app when possible. Use your app's existing storage, or an App Group if the companion app and extension both need to read the same values. Avoid putting networking, timers, long-running work, or account flows directly in the settings scene; let the companion app handle that work and post updated activity state through DynamicLakeActivityCenter.

6. Build The Compact View

Use LiveActivitySlotLayout so your content fits around the notch or pill gap. If the view is rendered from a provider, pass context.geometry through the layout and use liveActivityLeftSlotSymbol(in:) so hosted extension live activities get the same left-slot spacing as built-in Calendar. Pill mode changes both compact and sneak-peek width, so do not cache a width from another shape.

import DynamicLakeKit
import SwiftUI

struct ClockLiveActivityView: View {
    var body: some View {
        TimelineView(.periodic(from: .now, by: 1)) { timeline in
            GeometryReader { proxy in
                let geometry = LiveActivityGeometry.currentOnActiveScreen(
                    size: .large,
                    totalSize: CGSize(
                        width: max(proxy.size.width, 1),
                        height: max(proxy.size.height, 1)
                    )
                )

                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: timeline.date.formatted(date: .omitted, time: .shortened),
                        geometry: geometry,
                        fontSize: geometry.compactProminentTextFontSize,
                        weight: .semibold,
                        foregroundStyle: AnyShapeStyle(.white)
                    )
                }
                .frame(width: proxy.size.width, height: proxy.size.height)
            }
        }
    }
}

Avoid hard-coded panel widths, center gaps, or outer backgrounds. DynamicLake owns the panel.

7. Post Activity From The Companion App

The companion app opens the activity by posting the extension bundle identifier.

import DynamicLakeKit

enum ClockActivityPublisher {
    static let extensionBundleIdentifier = "com.example.ClockApp.ClockDynamicLakeExtension"
    static let activityIdentifier = "clock"

    static func open() {
        DynamicLakeActivityCenter.setActivityActive(
            true,
            extensionBundleIdentifier: extensionBundleIdentifier,
            activityIdentifier: activityIdentifier,
            title: "Clock",
            priority: .normal,
            supportsSneakPeek: true,
            size: .large
        )
    }

    static func close() {
        DynamicLakeActivityCenter.closeActivity(
            extensionBundleIdentifier: extensionBundleIdentifier,
            activityIdentifier: activityIdentifier
        )
    }
}

Call open() when the activity should become visible. Call close() when it should disappear.

If your UI needs changing values, define a small DynamicLakeLiveActivityState and send it with DynamicLakeActivityCenter/updateActivityState(extensionBundleIdentifier:activityIdentifier:state:encoder:isLiquidGlass:). See ActivityState.

8. Preview Before Installing

Create a provider for simulator previews.

struct ClockLiveActivityProvider: DynamicLakeLiveActivityViewProvider {
    typealias State = DynamicLakeEmptyLiveActivityState

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

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

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

Then show it in a normal macOS view:

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

Test both .notch and .pill. See Simulator.

9. Install And Test

Build the companion app. The built .app should contain the extension in one of these folders:

YourApp.app/Contents/PlugIns/YourExtension.appex
YourApp.app/Contents/Extensions/YourExtension.appex

If you installed DynamicLakeKit manually instead of using SPM, the app should also contain DynamicLakeKit:

YourApp.app/Contents/Frameworks/DynamicLakeKit.framework

Run or install the companion app, then open DynamicLake Pro settings and enable the extension under Extensions.

You can also test the extension with DynamicLake Playground. DynamicLake Playground is free, runs one extension or plugin at a time, and is intended for creator testing before using the full DynamicLake app.

If DynamicLake does not discover the extension after changing the extension point or bundle, fully quit both apps and force PlugInKit registration:

pluginkit -a "/path/to/YourApp.app"

Then launch the companion app and post an active activity.

Troubleshooting

| Symptom | Likely Cause | Fix | | --- | --- | --- | | Xcode shows No such module 'DynamicLakeKit' | The package product was not added to the target that imports DynamicLakeKit, or a manual framework path is missing. | Follow AddSPM and add DynamicLakeKit to both the companion app target and the extension target. Then clean the build folder and rebuild. | | Companion app shows "cannot be opened because of a problem" | A manually linked DynamicLakeKit framework is not embedded. | Set DynamicLakeKit to Embed & Sign on the companion app target. Check YourApp.app/Contents/Frameworks. | | DynamicLake settings does not show the extension | Wrong EXExtensionPointIdentifier, stale PlugInKit registration, or app is not installed where macOS can register it. | Verify the Info.plist value, rebuild, quit both apps, then run pluginkit -a "/path/to/YourApp.app". | | Extension appears in settings but no live activity appears | The companion app did not post setActivityActive(true, ...), or it used the companion app bundle identifier instead of the extension bundle identifier. | Use the extension bundle identifier with DynamicLakeActivityCenter. | | DynamicLake logs "extension not hostable" | ExtensionKit did not return a hostable identity. | Check signing, extension point binding, Info.plist, and PlugInKit registration. | | Compact surface says "Loading extension UI" or stays blank | Extension UI crashed, DynamicLakeKit is unavailable to the extension, or the scene ID is missing. | Run the companion app from Xcode, inspect crash logs, and verify the extension target depends on DynamicLakeKit. | | Sneak peek does not open | supportsSneakPeek was false in the configuration or activity post, or no DynamicLakeSneakPeek scene exists. | Set supportsSneakPeek: true in both places and add the scene. | | Works in simulator but not in DynamicLake Pro | Simulator only tests SwiftUI layout. It does not prove ExtensionKit discovery, signing, embedding, or activity posting. | Test the installed .app bundle and inspect DynamicLake logs. |

Release Checklist

Topics

Next Steps