Plugin Development Guide
Plugin Development Guide
This guide explains how to create a plugin for machNotch, demonstrating how to build a first-class feature that integrates seamlessly with the notch.
⚡️ The Philosophy
“Everything is a Plugin.”
Whether it’s the core Music player or a simple Battery indicator, all features are built using the same API available to third-party developers. This ensures the API is robust and capable.
🛠 Step-by-Step Implementation
1. Create the Plugin Struct
Create a new file in Plugins/BuiltIn/{MyFeature}Plugin/. It must conform to NotchPlugin.
import SwiftUI
@MainActor@Observablefinal class MyFeaturePlugin: NotchPlugin { // 1. Identity let id = "com.machnotch.myfeature"
let metadata = PluginMetadata( name: "My Feature", description: "Does something amazing", icon: "star.fill", // SF Symbol category: .productivity )
var isEnabled: Bool = true private(set) var state: PluginState = .inactive
// 2. Dependencies private var settings: PluginSettings? private var cancellables = Set<AnyCancellable>()
// 3. Lifecycle func activate(context: PluginContext) async throws { state = .activating self.settings = context.settings state = .active }
func deactivate() async { state = .inactive }}2. Define the UI
Plugins implement four UI slots via @ViewBuilder methods with concrete return types (no AnyView):
A. Closed Notch (Compact)
Shown inside the black notch bar. Space is limited.
@ViewBuilderfunc closedNotchContent() -> some View { if isEnabled, state.isActive { HStack { Image(systemName: "star.fill") Text("Active") } .foregroundStyle(.white) } else { EmptyView() }}B. Expanded Panel (Interactive)
Shown when the user hovers/clicks the notch. This provides a full canvas.
@ViewBuilderfunc expandedPanelContent() -> some View { if isEnabled, state.isActive { VStack { Text("My Amazing Feature") .font(.headline) Button("Do Action") { /* ... */ } } .padding() } else { EmptyView() }}C. Settings
Shown in the Settings panel for this plugin.
@ViewBuilderfunc settingsContent() -> some View { Toggle("Show icon", isOn: $showIcon)}D. Menu Bar
Items contributed to the app’s menu bar extra dropdown.
@ViewBuilderfunc menuBarView() -> some View { Button("My Action") { /* ... */ }}3. Requesting Display Time
The closed notch is a shared resource. Display must be requested.
Implement the displayRequest property:
var displayRequest: DisplayRequest? { guard isEnabled, state.isActive else { return nil }
// logic: only show if something important is happening if myFeatureIsRunning { return DisplayRequest( priority: .normal, // .background, .normal, .high, .critical category: .utility ) }
return nil}4. Accessing System Services
System APIs (like EventKit or CoreAudio) should not be accessed directly. Use the PluginContext.
func activate(context: PluginContext) async throws { // Get the shared calendar service let calendar = context.services.calendar
// Get the shared music service let music = context.services.music}5. Settings
Each plugin receives a sandboxed settings store.
func activate(context: PluginContext) async throws { self.settings = context.settings
// Read let showIcon = settings?.get("showIcon", default: true)}
func toggleIcon() { // Write settings?.set("showIcon", value: false)}🧪 Testing Your Plugin
Testing is mandatory. Since the plugin is a class, it can be unit tested easily.
@MainActorfinal class MyPluginTests: XCTestCase { func testActivation() async throws { let plugin = MyFeaturePlugin()
// Use mocks! let context = PluginContext.mock()
try await plugin.activate(context: context)
XCTAssertEqual(plugin.state, .active) }}📦 Registration
Finally, add the plugin to PluginRegistry.swift to register it. AppObjectGraph reads this registry when constructing PluginManager.
@MainActorenum PluginRegistry { static func makeBuiltInPlugins() -> [any NotchPlugin] { [ MusicPlugin(), MyFeaturePlugin() // <--- Add this ] }}``` register it. `AppObjectGraph` reads this registry when constructing `PluginManager`.
```swift// Plugins/Core/PluginRegistry.swift@MainActorenum PluginRegistry { static func makeBuiltInPlugins() -> [any NotchPlugin] { [ MusicPlugin(), MyFeaturePlugin() // <--- Add this ] }}