Building a clipboard manager sounds simple — watch the clipboard, save what gets copied, let the user paste old items. In practice, it touches almost every layer of macOS development: AppKit for system integration, SwiftUI for the interface, SwiftData for persistence, and Core Graphics for keystroke simulation.
This isn’t a step-by-step tutorial. It’s an architecture walkthrough — the decisions, tradeoffs, and patterns that go into building something like QuietClip. If you’re a developer considering building your own, this will save you a lot of dead ends.
Architecture overview
A clipboard manager has four main components:
These components are mostly independent. The monitor doesn’t know about the UI. The paste engine doesn’t know about storage. This separation keeps the codebase manageable.
Monitoring the clipboard
The clipboard monitor is the heartbeat of the app. It runs a timer — typically every 0.3 to 0.5 seconds — that checks whether NSPasteboard.general.changeCount has incremented.
@Observable
final class ClipboardMonitor {
private var lastChangeCount: Int
private var timer: Timer?
func start() {
lastChangeCount = NSPasteboard.general.changeCount
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
self?.checkForChanges()
}
}
private func checkForChanges() {
let current = NSPasteboard.general.changeCount
guard current != lastChangeCount else { return }
lastChangeCount = current
// Read and store the new content
}
}
There’s no notification API for clipboard changes — polling is the only option. The good news is that checking an integer is essentially free. Even at 0.3-second intervals, the CPU impact is unmeasurable.
Polling gets a bad reputation, but clipboard monitoring is the rare case where it’s genuinely the right approach — the API gives you no alternative, and the cost is negligible.
When a change is detected, you read all available types from the pasteboard. Text, images, file URLs, rich text — a single copy can carry multiple representations. Store the richest version you can, but always keep a plain text fallback for search.
Persisting with SwiftData
SwiftData is a natural fit for clipboard history. Each clipboard item becomes a model:
@Model
final class ClipItem {
var content: String
var imageData: Data?
var sourceApp: String?
var copiedAt: Date
var isPinned: Bool
}
SwiftData handles the SQLite backing store, migrations, and SwiftUI integration automatically. You get @Query for free — sorting by date, filtering by search text, limiting to pinned items — all with zero boilerplate.
Storing images
Images are the tricky part. Storing raw PNG data in the database works but can bloat the SQLite file. A better approach: store images as files on disk and keep the file path in SwiftData. This keeps the database fast for text queries while supporting large images. Set a reasonable maximum — QuietClip caps image storage at what fits within its item limit.
One important detail: set a maximum history size and enforce it. Without a cap, the database grows indefinitely. When inserting a new item, delete the oldest non-pinned item if you’ve hit the limit.
The SwiftUI panel
The UI is a floating panel — similar to Spotlight — that appears when the user presses the global hotkey. SwiftUI makes this straightforward:
- A
TextFieldfor search at the top - A
Listof clipboard items, sorted by recency - Keyboard navigation with arrow keys and Enter to paste
The challenge is hosting SwiftUI inside the right kind of window. You need an NSPanel — specifically one configured as .nonactivatingPanel — so that the original app retains focus. If you use a regular NSWindow, the target app loses focus and ⌘V pastes into your clipboard manager instead.
let panel = NSPanel(
contentRect: .zero,
styleMask: [.nonactivatingPanel, .fullSizeContentView],
backing: .buffered,
defer: true
)
panel.isFloatingPanel = true
panel.level = .floating
Then host your SwiftUI view inside it with NSHostingView.
Menu bar and AppKit
A clipboard manager lives in the menu bar, not the Dock. This means using NSStatusItem — which is still AppKit-only. Your app’s entry point will be an NSApplicationDelegate (or @NSApplicationDelegateAdaptor if you’re using SwiftUI’s App lifecycle) that creates the status item on launch.
let statusItem = NSStatusBar.system.statusItem(withLength: .squareLength)
statusItem.button?.image = NSImage(systemSymbolName: "clipboard", accessibilityDescription: "QuietClip")
Set LSUIElement to true in your Info.plist to hide the Dock icon. The app becomes menu-bar-only — present but unobtrusive.
The paste workflow
When the user selects a historical item, the paste workflow is:
- Write the item’s data back to
NSPasteboard.general - Dismiss the panel
- Wait a brief moment (10-50ms) for the panel to fully dismiss
- Simulate ⌘V using
CGEventto paste into the frontmost app
Step 4 requires Accessibility permissions. Without them, CGEvent.post() silently does nothing. Your app needs to request and guide the user through enabling Accessibility access in System Settings.
By keeping the panel as a non-activating NSPanel, the frontmost app never loses focus. When you simulate ⌘V, it goes straight to the right app — no need to track which app was active or manage focus switches.
This architecture — AppKit shell, SwiftUI views, SwiftData persistence, CGEvent paste — is how QuietClip is built. Each layer does one thing well, and the boundaries between them are clean.
See this architecture in action.
QuietClip is built with SwiftUI, SwiftData, and zero dependencies. Under 5 MB, runs on macOS 14+. Free to start, $8.99 once for Pro.