Extension Lifecycle
How registerModule works — render functions, lifecycle hooks, and action handlers.
Extension Lifecycle
Every extension calls DynamicIsland.registerModule() with an object describing how to render and respond to events.
registerModule
DynamicIsland.registerModule({
// Render functions (at least compact is required)
compact(): ViewNode,
expanded(): ViewNode,
fullExpanded(): ViewNode,
// Minimal compact (leading/trailing indicators)
minimalCompact: {
leading(): ViewNode,
trailing(): ViewNode,
},
// Lifecycle hooks
onActivate(): void,
onDeactivate(): void,
// Interaction handler
onAction(actionID: string, value?: unknown): void,
})Render functions
compact() — required
Called on every refreshInterval tick. Return a ViewNode describing the compact island content.
compact() {
const time = new Date().toLocaleTimeString();
return View.text(time, { style: "caption", color: "#e8e8e8" });
}expanded() — recommended
Called when the user hovers or taps the island. Return a richer view.
expanded() {
return View.vstack([
View.text("My Extension", { style: "headline" }),
View.text("Extended content here", { color: "#888" }),
], { spacing: 6 });
}fullExpanded() — optional
Called for the full expanded state. Falls back to expanded() if not provided.
minimalCompact — optional
For leading/trailing pill indicators (like iOS's camera/mic indicators).
minimalCompact: {
leading() {
return View.icon("circle.fill", { color: "#22c55e", size: 8 });
},
trailing() {
return View.text("On", { style: "caption2", color: "#22c55e" });
},
}Lifecycle hooks
onActivate()
Called when the extension becomes active (island starts showing this extension).
onActivate() {
console.log("Extension activated");
DynamicIsland.island.activate();
}onDeactivate()
Called when the extension is deactivated.
onDeactivate() {
// Cleanup, stop timers, etc.
}Action handler
onAction(actionID, value)
Called when an interactive view element fires. The actionID corresponds to the string you passed to View.button(), View.toggle(), or View.slider().
onAction(actionID, value) {
if (actionID === "toggle-timer") {
const running = DynamicIsland.store.get("running");
DynamicIsland.store.set("running", !running);
}
if (actionID === "set-volume") {
console.log("Volume set to:", value);
}
}Full example
let seconds = 0;
let running = false;
DynamicIsland.registerModule({
compact() {
const mins = Math.floor(seconds / 60).toString().padStart(2, "0");
const secs = (seconds % 60).toString().padStart(2, "0");
return View.hstack([
View.icon("timer", { size: 12, color: "#a855f7" }),
View.text(`${mins}:${secs}`, { style: "caption", color: "#e8e8e8" }),
], { spacing: 4 });
},
expanded() {
return View.vstack([
View.timerText(seconds, { style: "headline" }),
View.hstack([
View.button(View.text(running ? "Pause" : "Start"), "toggle"),
View.button(View.text("Reset"), "reset"),
], { spacing: 8 }),
], { spacing: 8 });
},
onActivate() {
running = DynamicIsland.store.get("running") ?? false;
seconds = DynamicIsland.store.get("seconds") ?? 0;
},
onAction(id) {
if (id === "toggle") {
running = !running;
DynamicIsland.store.set("running", running);
}
if (id === "reset") {
seconds = 0;
running = false;
}
},
});
setInterval(() => {
if (running) {
seconds++;
DynamicIsland.store.set("seconds", seconds);
}
}, 1000);