Version: 1.0.0
Platform: macOS 13.0+
Language: Swift 6.2
AtollExtensionKit enables third-party applications to display custom live activities and lock screen widgets inside the Atoll (DynamicIsland) app.
- Getting Started
- Authorization
- Live Activities
- Lock Screen Widgets
- Notch Experiences
- Priority System
- Best Practices
- Size Limits & Validation
- Error Handling
- Examples
Add AtollExtensionKit to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/ebullioscopic/AtollExtensionKit.git", from: "1.0.0")
]import AtollExtensionKitTwo sample targets under Samples mirror the workflows described in this document:
Samples/AtollXcodeSampleis a Swift Package CLI target. Runswift run --package-path Samples/AtollXcodeSampleto print the SDK version, build a descriptor, and validate thatAtollClient.sharedis reachable—ideal for sanity-checking your toolchain before touching UI code.Samples/AtollXcodeSampleAppis a SwiftUI macOS app. OpenAtollXcodeSampleApp.xcodeproj, build, and run to see the descriptor validator and client ping buttons we use when testing Sneak Peek copy, trailing content, and badge sizing. EditSources/App/ContentView.swiftto swap in your own descriptors (e.g., countdown text + progress indicator) and observe how Atoll renders them.
Use these projects as living documentation: copy/paste the descriptor snippets from this guide into the sample app to verify Sneak Peek colors, trailing bars, and badge sizes before integrating them into your product.
if AtollClient.shared.isAtollInstalled {
print("Atoll is available!")
}Before presenting any content, you must request user authorization:
do {
let authorized = try await AtollClient.shared.requestAuthorization()
if authorized {
print("User authorized this app!")
}
} catch {
print("Authorization failed: \(error)")
}- Request: Call
requestAuthorization()to show a permission dialog in Atoll - User Decision: User approves/denies in Atoll Settings → Extensions tab
- Result: Returns
Boolindicating authorization status
let isAuthorized = try await AtollClient.shared.checkAuthorization()AtollClient.shared.onAuthorizationChange = { isAuthorized in
print("Authorization changed: \(isAuthorized)")
}Live activities appear in the closed Dynamic Island notch, similar to timer, music, or reminder indicators.
- Authorize – call
requestAuthorization()as soon as practical and ask users to approve the Extensions permission inside Atoll if the call returnsfalse. - Assemble a descriptor – create an
AtollLiveActivityDescriptorwith a persistentid, concise title/subtitle, aleadingIcon(optionally swapped for another icon/Lottie vialeadingContent), trailing content (text/marquee/countdown/icon/animation), and optionalcenterTextStyle,sneakPeekConfig, or accent color. If you instead want a ring/bar/percentage on the right wing, settrailingContent = .noneand supply aprogressIndicator—the two are mutually exclusive. UseallowsMusicCoexistencewhen your activity can share space with music. Keep payloads lean to pass validation. - Validate/test – during development you can run
ExtensionDescriptorValidator.validate(_:)(part of the SDK) on sample descriptors or unit tests to catch size/length violations before shipping. - Present – send the descriptor via
presentLiveActivity(_:). Re-use the sameidfor the life of the session. - Update & dismiss – call
updateLiveActivity(_:)whenever the state changes, thendismissLiveActivity(activityID:)when the session ends so Atoll frees the slot. - Monitor callbacks & logs – subscribe to
onActivityDismissto detect user revocations, and enable Extension diagnostics logging inside Atoll → Settings → Extensions to see each payload, validation result, and display decision in Console.app.
Extension live activities support sneak peek – a temporary HUD that displays your title/subtitle when the activity appears or updates, preventing text from rendering behind the physical notch.
- Enable automatically – Omit
sneakPeekConfig(or set it to.default) to show title/subtitle in a brief HUD when your activity is presented. Text only appears via sneak peek, never under the notch hardware. - Custom duration – Provide
.standard(duration: 2.0)to control how long the sneak peek displays (in seconds). Inline requests (.inline(...)) are ignored for third-party descriptors and automatically downgraded to.standard. - Show on updates – Pass
AtollSneakPeekConfig(enabled: true, showOnUpdate: true)to trigger sneak peek every time you update the activity, not just on initial presentation. - Disable sneak peek – Set
sneakPeekConfig: .disabledto prevent automatic HUD displays. Your activity will still render in the closed notch, but the title/subtitle will be hidden to avoid text under the hardware. - Style override – Use
.standard()to force a specific presentation style, overriding the user's Atoll preference. Leavestyle: nilto inherit the user's setting. Inline overrides are downgraded to.standardfor third-party activities.
You can also override the HUD copy without changing the main descriptor text by setting sneakPeekTitle and sneakPeekSubtitle. These optional fields fall back to title / subtitle when omitted, allowing you to keep the notch copy short while presenting richer messaging inside the sneak peek.
Important: Atoll no longer renders extension titles/subtitles inside the closed notch. Even if you disable sneak peek, the center column stays empty so nothing ever collides with the hardware cutout. Always provide sneakPeekTitle / sneakPeekSubtitle (falling back to title / subtitle) so the HUD has copy to display.
Inline sneak peek layouts are now reserved for Atoll’s built-in experiences. Third-party descriptors always render the standard below-notch HUD even if you request inline, ensuring consistent text placement across the ecosystem.
- Sneak peek copy – Provide
sneakPeekTitle/sneakPeekSubtitle(falling back totitle/subtitle) so the HUD always has text to display. Since inline mode is disabled for extensions, the standard HUD is the sole place where copy appears. - Leading overrides – Use
leadingContentwhen you need to replace the default icon with anotherAtollIconDescriptoror a bundled Lottie animation. Text-based cases are rejected so the left wing always stays purely visual. - Music coexistence – Mark
allowsMusicCoexistence = truefor activities that can share space with music playback; Atoll will place your badge on the album art and shift the right wing automatically. - User-driven dismissals – Register
AtollClient.shared.onActivityDismissto learn when someone closes your activity using Atoll’s hover affordance. Shut down background work once this callback fires to avoid recreating the activity immediately.- Smooth animations – Activities appear with a subtle spring scale-in animation and fade-out on dismissal. Updates to the same activity ID animate smoothly without jarring transitions.
let activity = AtollLiveActivityDescriptor(
id: "workout-timer",
bundleIdentifier: Bundle.main.bundleIdentifier!,
priority: .normal,
title: "Workout Timer",
subtitle: "Chest & Triceps",
leadingIcon: .symbol(name: "figure.strengthtraining.traditional", color: .orange),
leadingContent: .icon(
.appIcon(
bundleIdentifier: "com.example.workout",
size: CGSize(width: 28, height: 28),
cornerRadius: 6
)
),
trailingContent: .marquee(
"Set 2 of 4",
font: .system(size: 12, weight: .medium),
minDuration: 0.5
),
accentColor: .orange,
badgeIcon: .symbol(name: "flame.fill", color: .orange),
allowsMusicCoexistence: true,
centerTextStyle: .inheritUser,
sneakPeekConfig: .standard(duration: 3.0), // Shows title/subtitle for ~3 seconds
sneakPeekTitle: "Workout timer",
sneakPeekSubtitle: "Set 2 of 4"
)
try await AtollClient.shared.presentLiveActivity(activity)With sneak peek on updates:
let activity = AtollLiveActivityDescriptor(
id: "download-progress",
title: "Downloading",
subtitle: "update-pkg-v2.dmg",
leadingIcon: .symbol(name: "arrow.down.circle.fill", color: .blue),
trailingContent: .none,
progressIndicator: .percentage(color: .blue),
progress: 0.45,
accentColor: .blue,
sneakPeekConfig: AtollSneakPeekConfig(
enabled: true,
duration: 2.0,
style: .standard,
showOnUpdate: true // Show sneak peek on every progress update
)
)var updated = activity
updated.subtitle = "5 reps remaining"
try await AtollClient.shared.updateLiveActivity(updated)try await AtollClient.shared.dismissLiveActivity(activityID: "workout-timer")AtollClient.shared.onActivityDismiss = { activityID in
print("Activity \(activityID) was dismissed by user")
}Text label:
.text(
"Running",
font: .system(size: 12, weight: .medium),
color: .accent // Optional; defaults to the descriptor's accent color
)Marquee text:
.marquee(
"Half Marathon Training",
font: .system(size: 12, weight: .semibold),
minDuration: 0.5,
color: .gray
)Countdown text:
.countdownText(
targetDate: Date().addingTimeInterval(3600),
font: .monospacedDigit(size: 13, weight: .semibold),
color: .green
)Icon:
.icon(.symbol(name: "timer", color: .green))Spectrum visualization:
.spectrum(color: .accent)Lottie animation:
.animation(data: lottieData, size: .init(width: 60, height: 32))None:
.noneℹ️
leadingContentonly accepts.iconand.animationso the left wing always renders a graphic (symbol, app icon, image, or Lottie) instead of text.
All text-based trailing cases (.text, .marquee, .countdownText) honor an optional color override so you can differentiate labels (e.g., red errors, green success) without altering the descriptor-wide accent color.
By default Atoll renders the leadingIcon you provide. Supplying leadingContent swaps the entire left wing for another AtollIconDescriptor (including .appIcon / .image) or a Lottie animation when you need richer artwork than the default glyph.
var descriptor = activity
descriptor.leadingContent = .icon(
.image(data: artworkPNGData, size: CGSize(width: 28, height: 28), cornerRadius: 6)
)
descriptor.badgeIcon = .symbol(name: "flame", color: .orange)AtollCenterTextStyle remains part of the data model for forward compatibility, but the Atoll host ignores it for extension live activities—the center column stays blank while the closed notch is visible. Use sneak peek text instead of relying on this property for active layouts.
Progress indicators occupy the right wing whenever trailingContent == .none. If you provide any trailing content, the indicator is ignored and validation fails, ensuring the wing always renders a single visual element.
Ring (circular):
.ring(diameter: 26, strokeWidth: 3, color: .accent)Bar (horizontal):
.bar(width: 90, height: 4, cornerRadius: 2, color: .orange)Percentage text:
.percentage(font: .system(size: 13, weight: .bold), color: .accent)Countdown timer:
.countdown(font: .monospacedDigit(size: 13, weight: .semibold), color: .accent)Lottie animation:
.lottie(animationData: animationData, size: CGSize(width: 32, height: 32))None:
.noneEvery indicator except .lottie and .none accepts an optional color override so you can align the bar/ring/text tint with the semantic state you are representing without changing the descriptor-wide accent color.
Widgets appear on the macOS lock screen similar to weather, music, or battery indicators.
let widget = AtollLockScreenWidgetDescriptor(
id: "stock-ticker",
bundleIdentifier: Bundle.main.bundleIdentifier!,
layoutStyle: .card,
position: .init(alignment: .leading, verticalOffset: -80, horizontalOffset: 60),
size: CGSize(width: 220, height: 110),
material: .frosted,
appearance: .init(
tintColor: .white,
tintOpacity: 0.12,
enableGlassHighlight: true,
contentInsets: .init(top: 14, leading: 18, bottom: 14, trailing: 18),
border: .init(color: .white, opacity: 0.18, width: 1),
shadow: .init(color: .black, opacity: 0.35, radius: 26, offset: CGSize(width: 0, height: -10))
),
cornerRadius: 18,
content: [
.icon(.symbol(name: "chart.line.uptrend.xyaxis", color: .green)),
.text("AAPL", font: .system(size: 16, weight: .semibold), color: .white),
.text("$175.43", font: .system(size: 22, weight: .bold), color: .green),
.text("+2.3%", font: .system(size: 14, weight: .medium), color: .green, alignment: .trailing),
.webView(
.init(
html: "<div class=\"sparkline\"></div><script>renderSparkline()</script>",
preferredHeight: 90,
isTransparent: true,
allowLocalhostRequests: false
)
)
],
accentColor: .accent,
dismissOnUnlock: true,
priority: .normal
)
try await AtollClient.shared.presentLockScreenWidget(widget).inline– single-line layout similar to Atoll’s weather widgets (default size: 200×48 pt).circular– compact circular badges for gauges or progress indicators (default: 100×100 pt).card– rectangular surface for richer compositions (default: 220×120 pt).custom– opt-in when you want full control over the size (defaults to 150×80 pt; still clamped to 640×360 pt)
Icon:
.icon(.symbol(name: "bolt.fill", color: .yellow))Text:
.text("Battery", font: .system(size: 14, weight: .medium), color: .white)Progress:
.progress(.bar(width: 120, height: 4), value: 0.75, color: .green)Graph:
.graph(data: [0.2, 0.5, 0.8, 0.6], color: .blue, size: CGSize(width: 160, height: 60))Gauge:
.gauge(value: 0.6, minValue: 0, maxValue: 1, style: .circular, color: .orange)Spacer:
.spacer(height: 8)Divider:
.divider(color: .gray, thickness: 1)Web View:
.webView(
.init(
html: "<div class=\"now-playing\">…</div>",
preferredHeight: 110,
isTransparent: true,
allowLocalhostRequests: true
)
)- Alignment-aware offsets –
AtollWidgetPositionaccepts an alignment (leading,center,trailing) plusverticalOffset(±400 pt) andhorizontalOffset(±600 pt). Use these fields instead of screen coordinates so widgets remain notch-safe across displays. SetclampModeto.relaxedor.unconstrainedwhen you need to escape the default safe-area margins. - Material presets –
AtollWidgetMaterialincludes.frosted,.liquid,.solid,.semiTransparent, and.clear. Pair liquid material with larger corner radii (≥20 pt) to mirror Atoll’s glass overlays, toggleappearance.enableGlassHighlightwhen you need macOS to add the highlight to other materials, and specifyappearance.liquidGlassVariantto request a particular Apple liquid-glass variant whenevermaterial == .liquid. - Deterministic sizing – Provide a custom
sizewhen you need dimensions outside the layout style defaults. The SDK clamps all widgets to 640×360 pt to avoid overlap.
- Tint overlays –
appearance.tintColorandtintOpacityadd a translucent color wash above frosted/liquid materials without mutating the descriptor-wide accent. - Content insets – Supply
appearance.contentInsets(top/leading/bottom/trailing) to override the default padding and keep text/gauges perfectly aligned with your design system. - Borders & shadows – Use
AtollWidgetBorderStyle/AtollWidgetShadowStyleto specify custom borders and drop shadows. Whenappearance.borderis omitted, Atoll falls back to a 1 pt white stroke (4% opacity). - Glass accents – Enable
appearance.enableGlassHighlightto request the macOS Liquid Glass treatment wherever available. On older systems the view gracefully falls back to.regularMaterial. - Liquid glass variants – Set
appearance.liquidGlassVariant = AtollLiquidGlassVariant(value)to match the user-facing “Custom Liquid Glass” slider (values clamp to 0–19). The host quietly falls back to standard liquid when the user disables custom variants or the requested value is unavailable.
Use AtollLiquidGlassVariant to request one of Apple’s undocumented liquid-glass kernels (0–19) so your widgets line up with the in-app customization sliders. Values outside the supported range clamp automatically, keeping your descriptors valid even if you reuse persisted settings across devices.
let widget = AtollLockScreenWidgetDescriptor(
id: "dashboard",
bundleIdentifier: Bundle.main.bundleIdentifier!,
layoutStyle: .card,
material: .liquid,
appearance: .init(
enableGlassHighlight: true,
liquidGlassVariant: AtollLiquidGlassVariant(12),
tintColor: .white,
tintOpacity: 0.08
),
content: [...]
)- Only liquid materials honor the variant. When
materialswitches away from.liquid, Atoll ignores the variant and keeps rendering the requested material. - Users can force “Standard Liquid Glass” inside Atoll’s settings. In that mode the host discards the variant but preserves your other appearance overrides, so no additional migration is required on your side.
- Always provide a sane
cornerRadius(≥20 pt recommended) so pronounced variants retain their curved reflections instead of clipping against sharp edges.
- Stay in sync with Atoll –
appearance.liquidGlassVariantmaps directly to Atoll’s “Custom Liquid Glass” slider, so requesting the same integer keeps third-party widgets visually aligned with the host music/timer panels. Values outside 0–19 clamp automatically. - Respect user overrides – When a user disables custom liquid glass or forces the “Standard” preset, Atoll silently ignores the variant while leaving your tint/border/shadow settings intact. No additional update call is needed.
- Material-aware – Only set
appearance.liquidGlassVariantwhenmaterial == .liquid. The field is ignored for.frosted,.solid,.semiTransparent, and.clearsurfaces. - Highlight pairing – Combine
appearance.enableGlassHighlight = truewith rounded corners (≥20 pt) and a low-opacity tint to mirror Atoll’s own lock screen look. - Sample
let variant = AtollLiquidGlassVariant(UserDefaults.standard.integer(forKey: "glassPreset"))
let widget = AtollLockScreenWidgetDescriptor(
id: "charging-panel",
bundleIdentifier: Bundle.main.bundleIdentifier!,
layoutStyle: .card,
material: .liquid,
appearance: .init(
enableGlassHighlight: true,
liquidGlassVariant: variant,
tintColor: .white,
tintOpacity: 0.05
),
content: [...]
)This setup lets extensions reuse the same numeric preset Atoll surfaces expose in Settings → Lock Screen → Custom Liquid Glass, keeping the lock screen cohesive for users who tweak materials per panel.
- Sandboxed WKWebView –
.webViewrenders inline HTML/CSS/JS (max 20 KB) inside a mouse-disabled WKWebView so you can layer custom shaders, charts, or canvas effects over Atoll’s background. - Network policy – Navigation is limited to
about:/data:URLs unless you setallowLocalhostRequests = true, which whitelistshttp://localhostandhttp://127.0.0.1for dev servers. All other hosts are blocked for safety. - Remote policy – Set
allowRemoteRequests = truewhen your payload needs remote libraries (for example CDN-hosted scripts/styles). Keep this disabled unless required. - Visual control –
isTransparentclears the view’s background so only your markup appears. ProvidebackgroundColorwhen you need an explicit fill, and usemaximumContentWidthto clamp how wide the surface can stretch. - Sizing –
preferredHeightdrives the web view’s height (clamped to 40–420 pt). Combine it with layoutsize+ padding to keep the chrome balanced.
- Mix and match elements – Compose
.text,.icon,.progress,.graph,.gauge,.spacer, and.dividerentries to create layered widgets without embedding executable UI code. - Bring your own chrome – Use
.webViewfor transparent HTML/CSS/JS overlays (think vector gradients, live charts, or sparkline canvases) while keeping gestures disabled and respecting Atoll’s security policy. - Use gauges for live metrics –
.gaugeoutputs circular or linear indicators with independent min/max ranges, perfect for weather, battery, or fitness statistics. - Respect color limits – Stick to
AtollColorDescriptorvalues so Atoll can enforce monochrome/high-contrast modes on colorful wallpapers. - Keep it light – Each widget supports up to 20 content elements. Prefer summaries over dense graphs when possible to minimize rendering cost.
.frosted– translucent blur that mirrors Atoll’s default overlays.liquid– high-gloss “liquid glass” treatment for hero widgets.solid– opaque background using the widget’s accent color.semiTransparent– subtle tint with reduced opacity.clear– fully transparent background, ideal for minimalist text/icon layouts
AtollNotchExperienceDescriptor lets you surface rich, structured content directly inside Atoll’s Dynamic Island. A descriptor can render:
- A standard notch tab that sits alongside built-in tabs (Timers, Shelf, etc.)
- An optional minimalistic override that replaces the compact music UI while the user’s minimalistic mode is active
Both surfaces share the same declarative building blocks as lock screen widgets, so SDK clients never send executable UI code—only structured content sections, optional icons, and sandboxed web payloads.
- Requires the user to enable Extensions → Allow extension notch experiences inside Atoll Settings. Users can further toggle tabs, minimalistic overrides, and interactive web views individually; always provide a fallback path in your app.
- Capacity is limited (default: 2 simultaneous experiences). Submit only when there is meaningful information to show and dismiss promptly when stale.
- Tabs and minimalistic overrides are independent. You can ship one, the other, or both in the same descriptor.
- Content is rendered by Atoll; titles and copy never sit under the physical notch. Sneak Peek HUD handles headline text when tabs appear.
- Authorize – Request authorization just like live activities. Notch experiences honor the same permission scope.
- Assemble the descriptor – Populate the required metadata plus either
tab,minimalistic, or both configurations. - Validate locally – Call
descriptor.isValidorExtensionDescriptorValidator.validate(_:)in your tests to catch layout/length issues before hitting the service. - Present –
try await AtollClient.shared.presentNotchExperience(descriptor)queues the experience. Use a stableidper logical surface so updates replace the existing tab instead of creating duplicates. - Update – Re-send the descriptor with new content via
updateNotchExperience(_:). Prefer incremental updates over dismiss/re-present to keep animations smooth. - Dismiss – Call
dismissNotchExperience(experienceID:)when the session ends, or respond toonNotchExperienceDismisscallbacks if the user revokes it from Atoll. - Fallbacks – When the user disables tabs, minimalistic overrides, or interactive web content, degrade gracefully inside your own UI instead of re-presenting.
Top-level fields mirror other Atoll descriptors:
idandbundleIdentifieruniquely identify your experience.prioritydetermines ordering relative to other extension tabs (same enum as live activities).accentColortints dividers, highlights, and fallback UI elements.metadatacarries up to 32 key/value pairs for diagnostics (never rendered to the user).tab/minimalistichold their respective configurations. At least one must be present.
- Presentation – Tabs appear in Atoll’s Tab bar when
Enable extension notch tabsis on. Users tap the tab to show your content; Atoll hides it automatically when the descriptor disappears. - Layout – Provide up to 6
AtollNotchContentSectionentries. Each section can be astack,columns, ormetricslayout and accepts the sameAtollWidgetContentElementpayloads as lock screen widgets (text, icons, graphs, gauges, progress, spacers, dividers, web views). - Sizing –
preferredHeightsuggests how tall the tab should be (clamped to 160–420 pt). Atoll ensures the size stays within the notch frame. - Branding – Use
iconSymbolName,badgeIcon, andappearanceto align with your app’s look. Keep labels short for accessibility. - Footnotes – Optional footnote text (≤140 characters) appears beneath your content stack for legal copy or instructions.
- Interactive web content – Supply
webContentplusallowWebInteraction = truewhen your tab needs a sandboxed WKWebView with keyboard/mouse input. Atoll rejects descriptors that contain web content if the user disabled Allow interactive web content.
- Use case – Override the compact minimalistic music layout with extension-driven content while the user’s minimalistic mode is active.
- Copy – Optional
headline(≤80 chars) andsubtitle(≤120 chars) sit above your sections, mirroring the music title/subtitle area without touching the physical notch. - Sections – Provide up to 3 sections with the same content elements as tabs. Minimalistic sections should remain lightweight to avoid overcrowding.
- Web content – Optional
webContentrenders a sandboxed view sized automatically by Atoll. It respects the same global interactive web toggle as tabs. - Layout hints –
layoutcommunicates the general form factor (.stack,.metrics,.custom) so Atoll can adjust padding. UsehidesMusicControlsif you need the music buttons removed entirely.
AtollNotchContentSectionlimits you to 6 elements per section. Each element is one of the existing widget building blocks (.text,.icon,.progress,.graph,.gauge,.webView,.divider,.spacer).- Titles (≤80 chars) and subtitles (≤160 chars) are optional per section. Use them sparingly to keep the notch readable.
- Because these types are
Codable, you can reuse existing widget-building utilities when assembling your notch descriptors.
- HTML payloads share the lock screen widget limits (20 KB max, inline assets only).
- Navigation remains limited to
about:/data:unless you explicitly allow localhost insideAtollWidgetWebContentDescriptor(useful for pointing at a dev server during testing). - Set
allowWebInteraction = trueonly when you genuinely need keyboard or mouse input. Tabs default to read-only web views. - Atoll rejects descriptors that include web content when the user disables Allow interactive web content or when system security policies block the payload. Always render equivalent data using native elements whenever possible.
let descriptor = AtollNotchExperienceDescriptor(
id: "finance-dashboard",
priority: .high,
accentColor: .init(red: 0.18, green: 0.65, blue: 0.94),
tab: .init(
title: "Finance",
iconSymbolName: "chart.pie.fill",
preferredHeight: 260,
sections: [
.init(
id: "positions",
title: "Positions",
layout: .columns,
elements: [
.text("AAPL", font: .system(size: 16, weight: .semibold), color: .white),
.text("$182.44", font: .monospacedDigit(size: 16, weight: .medium), color: .green),
.gauge(value: 0.72, minValue: 0, maxValue: 1, style: .circular, color: .green),
.divider(color: .white, thickness: 1)
]
)
],
webContent: .init(
html: "<canvas id=\"spark\"></canvas><script>renderSpark()</script>",
preferredHeight: 120,
allowLocalhostRequests: false,
isTransparent: true
),
allowWebInteraction: false
),
minimalistic: .init(
headline: "Portfolio",
subtitle: "Daily change +$1,820",
sections: [
.init(
id: "overview",
layout: .metrics,
elements: [
.text("Top mover", font: .system(size: 13, weight: .regular), color: .white),
.text("+3.4%", font: .monospacedDigit(size: 15, weight: .semibold), color: .green)
]
)
],
hidesMusicControls: true
)
)
try await AtollClient.shared.presentNotchExperience(descriptor)try await AtollClient.shared.presentNotchExperience(descriptor)
try await AtollClient.shared.updateNotchExperience(descriptor)
try await AtollClient.shared.dismissNotchExperience(experienceID: descriptor.id)
AtollClient.shared.onNotchExperienceDismiss(experienceID: descriptor.id) {
// Cleanup work / update UI
}Use dismissal callbacks to stop background work when the user closes your tab from Atoll. If you re-present immediately, Atoll treats it as a new submission and re-applies validation/capacity checks.
When multiple live activities compete for space, priority determines visibility:
| Priority | Use Case | Examples |
|---|---|---|
.critical |
Time-sensitive alerts | Timers at 0:00, critical reminders |
.high |
Important ongoing tasks | Active workouts, cooking timers |
.normal |
Standard activities | Music playback, background tasks |
.low |
Informational updates | Download progress, syncing |
- Higher priority always wins when space is limited
- Activities with
.allowsMusicCoexistence = truecan share space with music - Equal priority → newest activity takes precedence
- User can manually dismiss any activity regardless of priority
- Don't overuse
.critical— reserve for genuinely urgent content - Most activities should use
.normal
- Titles: 1-3 words recommended
- Subtitles: 3-7 words maximum
- Trailing content should be scannable at a glance
- Use SF Symbols when possible for consistency
- Keep custom images under 100KB
- Avoid complex multi-color icons
let descriptor = AtollLiveActivityDescriptor(
id: "timer",
bundleIdentifier: Bundle.main.bundleIdentifier!,
title: "Timer",
leadingIcon: .symbol(name: "timer", color: .blue),
allowsMusicCoexistence: true
)Set allowsMusicCoexistence = true for activities that should appear alongside music playback.
- Batch multiple property changes into one
updateLiveActivity()call - Don't update more than once per second
- Dismiss activities when no longer needed
do {
try await AtollClient.shared.presentLiveActivity(activity)
} catch AtollExtensionKitError.notAuthorized {
// Prompt user to authorize in Atoll settings
} catch AtollExtensionKitError.atollNotInstalled {
// Show install prompt
} catch {
// Handle other errors
}- Listen for
onActivityDismisscallbacks - Don't immediately re-present dismissed activities
- Provide in-app settings to disable live activities
- Leave
centerTextStyleat.inheritUserwhenever possible so the view respects the user's Sneak Peek preference; only force.inlineor.standardwhen your layout requires a specific treatment.
- Users can toggle notch experiences, extension tabs, minimalistic overrides, and interactive web content independently inside Atoll. Detect
AtollExtensionKitError.invalidDescriptor/AtollExtensionKitError.notAuthorizedresponses and keep rendering equivalent information inside your own UI instead of looping on retries. - Avoid presenting placeholder tabs just to reserve capacity. Submit descriptors only when you have live data to show and dismiss them when finished.
| Property | Limit | Notes |
|---|---|---|
| Title | 50 characters | Truncated if longer |
| Subtitle | 100 characters | Optional |
| Icon image data | 5 MB | Validation enforced |
| Lottie JSON (Base64) | 5 MB | Animation data |
| Activity duration | 24 hours max | Auto-dismissed after |
| Update rate | 1/second | Throttled server-side |
| Property | Limit | Notes |
|---|---|---|
| Widget width | 640 pt max | Enforced |
| Widget height | 360 pt max | Enforced |
| Content elements | 20 max | Performance |
| Text length | 100 chars | Per element |
| Image data | 5 MB | Per icon |
| Graph data points | 100 max | Performance |
| Web content HTML | 20 KB | .webView payload |
| Property | Limit | Notes |
|---|---|---|
| Concurrent experiences | 2 global (default) | Host-enforced capacity |
| Tab sections | 6 max | Each section must be valid |
| Section elements | 6 max | Shares rules with widget elements |
| Tab preferred height | 160–420 pt | Clamped by host |
| Tab footnote | 140 characters | Optional |
| Minimalistic sections | 3 max | Keep content concise |
| Minimalistic headline | 80 characters | Optional |
| Minimalistic subtitle | 120 characters | Optional |
| Section title | 80 characters | Optional |
| Section subtitle | 160 characters | Optional |
| Web content HTML | 20 KB | Same limits as widgets |
AtollExtensionKit validates all descriptors before sending to Atoll:
catch AtollExtensionKitError.invalidDescriptor(let reason) {
print("Validation failed: \(reason)")
}enum AtollExtensionKitError: LocalizedError {
case atollNotInstalled
case notAuthorized
case serviceUnavailable
case connectionFailed(underlying: Error)
case invalidDescriptor(reason: String)
case activityNotFound(activityID: String)
case widgetNotFound(widgetID: String)
case unknown(String)
}Atoll Not Installed:
catch AtollExtensionKitError.atollNotInstalled {
let alert = NSAlert()
alert.messageText = "Atoll Required"
alert.informativeText = "Please install Atoll from atoll.app"
alert.runModal()
}Not Authorized:
catch AtollExtensionKitError.notAuthorized {
// Prompt user to open Atoll Settings → Extensions
}Service Unavailable:
catch AtollExtensionKitError.serviceUnavailable {
// Atoll might be quitting or updating, retry later
}import AtollExtensionKit
class PomodoroManager {
let client = AtollClient.shared
func startPomodoro() async throws {
let activity = AtollLiveActivityDescriptor(
id: "pomodoro-\(UUID())",
bundleIdentifier: Bundle.main.bundleIdentifier!,
priority: .high,
title: "Focus Time",
subtitle: "Deep Work Session",
leadingIcon: .symbol(name: "brain.head.profile", color: .purple),
trailingContent: .countdownText(
targetDate: Date().addingTimeInterval(25 * 60),
font: .monospacedDigit(size: 14, weight: .semibold)
),
accentColor: .purple,
allowsMusicCoexistence: true
)
try await client.presentLiveActivity(activity)
}
}func showDownload(filename: String, progress: Double) async throws {
let activity = AtollLiveActivityDescriptor(
id: "download-\(filename)",
bundleIdentifier: Bundle.main.bundleIdentifier!,
priority: .low,
title: "Downloading",
subtitle: filename,
leadingIcon: .symbol(name: "arrow.down.circle.fill", color: .blue),
trailingContent: .none,
progressIndicator: .bar(width: 110, height: 4),
progress: progress,
accentColor: .blue
)
try await AtollClient.shared.updateLiveActivity(activity)
}func showCryptoWidget(symbol: String, price: Double, change: Double) async throws {
let isPositive = change >= 0
let color: AtollColorDescriptor = isPositive ? .green : .red
let widget = AtollLockScreenWidgetDescriptor(
id: "crypto-\(symbol)",
bundleIdentifier: Bundle.main.bundleIdentifier!,
layoutStyle: .inline,
position: .init(alignment: .center, verticalOffset: 100),
material: .frosted,
content: [
.icon(.symbol(name: "bitcoinsign.circle.fill", color: .orange)),
.spacer(height: 4),
.text(symbol, font: .system(size: 16, weight: .bold), color: .white),
.spacer(height: 6),
.text(
"$\(String(format: "%.2f", price))",
font: .monospacedDigit(size: 16, weight: .semibold),
color: .white
),
.spacer(height: 4),
.text(
String(format: "%+.2f%%", change),
font: .monospacedDigit(size: 14, weight: .medium),
color: color
)
]
)
try await AtollClient.shared.presentLockScreenWidget(widget)
}func startWorkout() async throws {
let activity = AtollLiveActivityDescriptor(
id: "workout-\(Date().timeIntervalSince1970)",
bundleIdentifier: Bundle.main.bundleIdentifier!,
priority: .high,
title: "Workout",
subtitle: "Upper Body",
leadingIcon: .symbol(name: "figure.strengthtraining.traditional", color: .orange),
trailingContent: .text("142 bpm"),
progressIndicator: .percentage(
font: .system(size: 14, weight: .bold, design: .rounded)
),
accentColor: .orange,
allowsMusicCoexistence: true,
metadata: ["startTime": "\(Date())"]
)
try await AtollClient.shared.presentLiveActivity(activity)
}Check the Atoll version at runtime:
let version = try await AtollClient.shared.getVersion()
print("Atoll version: \(version)")Minimum supported Atoll version: 1.0.0
- Website: https://atoll.app
- Documentation: https://docs.atoll.app
- GitHub: https://github.com/ebullioscopic/AtollExtensionKit
- Issues: https://github.com/ebullioscopic/AtollExtensionKit/issues
AtollExtensionKit is available under the MIT license. See LICENSE for details.