Build polished, notch-aware surfaces for macOS apps with SwiftUI.
DynamicNotch gives you a reusable SwiftUI surface and an AppKit-backed window controller for edge-attached experiences: recording indicators, media controls, build progress, approvals, notifications, and Dynamic Island-style compact states.
Notch-based UI looks simple until window coordinates, rounded shoulders, clipping, hit testing, safe areas, and physical camera housings all meet in one component.
DynamicNotch keeps those responsibilities in the right places:
- SwiftUI owns your content.
- AppKit owns the floating window and screen geometry.
- The package clips rendering and hit testing to the same notch silhouette.
- Top and bottom windows account for the selected display's safe area.
- Compact mode reserves the physical camera housing while keeping the background continuous.
- macOS 13 or later
- Swift 5.9 or later
- A SwiftUI macOS app with AppKit access
- Open your app project in Xcode.
- Select File → Add Package Dependencies….
- Paste the GitHub repository URL for DynamicNotch.
- Choose a version or branch.
- Add the
DynamicNotchproduct to your macOS target.
Then import it:
import DynamicNotchAdd the repository as a dependency in your Package.swift:
dependencies: [
.package(
url: "https://github.com/govijr/DynamicNotch-MacOS.git",
from: "0.1.0"
)
]Add the product to the target that uses it:
targets: [
.executableTarget(
name: "YourMacApp",
dependencies: [
.product(name: "DynamicNotch", package: "DynamicNotch")
]
)
]The package is published at github.com/govijr/DynamicNotch-MacOS.
Use DynamicNotch when your app already owns the containing window:
import SwiftUI
import DynamicNotch
struct RecordingSurface: View {
var body: some View {
DynamicNotch(
direction: .top,
width: 420,
height: 72,
background: .black,
contentInsets: EdgeInsets(
top: 0,
leading: 18,
bottom: 0,
trailing: 18
)
) {
HStack(spacing: 10) {
Circle()
.fill(.green)
.frame(width: 8, height: 8)
Text("Recording")
.foregroundStyle(.white)
Spacer()
Text("00:42")
.foregroundStyle(.white.opacity(0.7))
.monospacedDigit()
}
}
}
}Supported directions are .top, .bottom, .left, and .right.
Use DynamicNotchWindowController when the surface should float above the desktop:
import AppKit
import DynamicNotch
import SwiftUI
@MainActor
final class NotchPresenter {
private let controller = DynamicNotchWindowController()
func showBuildStatus() {
controller.show(
placement: .edge(.top, alignment: .center),
width: 420,
height: 72
) {
HStack(spacing: 10) {
Circle()
.fill(.green)
.frame(width: 8, height: 8)
Text("Build finished")
.foregroundStyle(.white)
Spacer()
Image(systemName: "checkmark")
.foregroundStyle(.green)
}
.padding(.horizontal, 20)
}
}
func hide() {
controller.hide()
}
}Keep one controller alive for the lifetime of the feature. Calling show again updates the content and animates the window to its new geometry.
DynamicNotchPlacement controls the edge, alignment, and offset:
.edge(.top, alignment: .center)
.edge(.bottom, alignment: .end, offset: -24)
.edge(.left, alignment: .start, offset: 16)
.edge(.right, alignment: .end)start, center, and end are relative to the attached edge. Positive offsets move toward the edge's end: rightward on horizontal edges and downward on vertical edges. Placement is clamped to the selected screen.
Pass screen: when targeting a particular display in a multi-monitor setup. Otherwise DynamicNotch uses the main screen.
The window controller automatically uses the selected screen's safe-area inset for top and bottom surfaces. This keeps expanded content below a physical camera housing and keeps bottom content away from the exposed edge.
controller.show(
placement: .edge(.top),
width: 420,
height: 72
) {
Text("Camera-safe content")
.foregroundStyle(.white)
}When using DynamicNotch directly, pass the inset yourself because a standalone SwiftUI view cannot infer its containing screen:
let safeTopInset = NSScreen.main?.safeAreaInsets.top ?? 3
DynamicNotch(
direction: .top,
width: 420,
height: 72,
attachedEdgeRail: safeTopInset
) {
Text("Camera-safe content")
}Set attachedEdgeRail: 0 when you explicitly want to opt out of the automatic controller behavior.
Compact mode is designed for the small, always-present state of a top notch. It provides independent leading and trailing regions and automatically measures the physical camera gap when the display exposes it.
The background remains one continuous surface; the center gap affects content layout only, so the physical notch never reveals a transparent hole.
controller.showCompact(width: 400) {
HStack(spacing: 6) {
Circle()
.fill(.green)
.frame(width: 7, height: 7)
Text("Live")
}
.foregroundStyle(.white)
} trailing: {
HStack(spacing: 8) {
Image(systemName: "waveform")
Text("00:42")
.monospacedDigit()
}
.foregroundStyle(.white.opacity(0.75))
}The compact height defaults to the larger of the status-bar thickness and the display's top safe area. Override height or centerGap when you are building a fixed hardware-independent layout.
Move between modes using the same controller:
controller.showCompact(width: 400) {
CompactLeadingView()
} trailing: {
CompactTrailingView()
}
controller.show(
placement: .edge(.top),
width: 520,
height: 180
) {
ExpandedView()
}The controller exposes presentationMode as .compact or .expanded.
All surfaces support a rounded exposed edge and independent shoulder curvature:
DynamicNotch(
direction: .top,
width: 520,
height: 180,
cornerRadius: 25,
shoulderRadius: 30,
background: .black
) {
Text("Expanded content")
.foregroundStyle(.white)
}Use cornerRadius for the exposed corners and shoulderRadius for the inward transitions from the attached lip.
The package includes a small demo with:
- top compact mode
- top expanded mode
- bottom playback mode
- left build progress
- right approval action
Run it from the package directory:
swift run DynamicNotchDemoTo build a normal Finder-style app bundle:
zsh Scripts/build-demo-app.sh
open dist/DynamicNotchDemo.appDynamicNotch provides the geometry, clipping, safe-area handling, placement, and presentation primitives. The consuming app owns product behavior such as:
- hover expansion
- drag gestures
- notifications and timers
- persistence
- menu-bar integration
- application-specific transitions and state
Keeping those decisions in the consuming app keeps the package small and makes it work for more kinds of macOS apps.
DynamicNotch is currently intended for early adopters and production prototypes. The core API is stable enough to integrate, while the package continues to evolve toward a 1.0 release with broader integration and visual test coverage.
DynamicNotch is available under the MIT License.
Copyright (c) 2026 Govi
See LICENSE for the full license text.