DEV Community

Rutvik Panchal
Rutvik Panchal

Posted on Fully Autonomous

I built a MacBook notch app in SwiftUI. Here's what the notch taught me

The notch on my MacBook Pro was just sitting there, a black rectangle in the middle of the menu bar. Meanwhile I spent my days doing two things over and over: checking when my next call starts, and digging for something I copied ten minutes ago.

So I built NotchDesk: hover the notch and you get your next meeting with a live countdown and a Join button, plus a private clipboard history you can paste from in any app. It's a native Mac app (SwiftUI + AppKit, macOS 14+), and it launches this week.

This post isn't a sales pitch. It's the handful of things I had to figure out that I couldn't find written down in one place. If you're thinking of building anything that lives in or around the notch, I hope it saves you a weekend.

1. Finding the notch (and measuring it)

There's no NSScreen.hasNotch. What you do get, since macOS 12, is safeAreaInsets and two "auxiliary" areas: the strips of menu bar to the left and right of the camera housing. The notch is whatever's left in the middle:

if screen.safeAreaInsets.top > 0 {
    let left = screen.auxiliaryTopLeftArea?.width ?? 0
    let right = screen.auxiliaryTopRightArea?.width ?? 0
    let width = screen.frame.width - left - right
    notchSize = CGSize(width: width > 0 ? width : 200,
                       height: screen.safeAreaInsets.top)
} else {
    // No notch: external display, lid closed, or an older Mac.
    // Draw a small "island" under the menu bar instead.
    let menuBarHeight = screen.frame.maxY - screen.visibleFrame.maxY
    notchSize = CGSize(width: 130, height: max(menuBarHeight, 24))
}
Enter fullscreen mode Exit fullscreen mode

Two things bit me:

  • The notch moves between screens. Close the lid with an external display attached and the notched screen disappears. Listen for NSApplication.didChangeScreenParametersNotification and re-place your window every time.
  • Plenty of your users don't have a notch. M1 Airs, external displays, the new MacBook Neo. Decide early what your app looks like without one. I went with a small island at the top centre of the main display, and it ended up being a setting people actually use on their external monitors.

2. A window that lives above the menu bar

The notch overlay is a borderless NSPanel that never steals focus:

let panel = NSPanel(contentRect: rect,
                    styleMask: [.borderless, .nonactivatingPanel],
                    backing: .buffered, defer: false)
panel.level = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue + 3)
panel.collectionBehavior = [.canJoinAllSpaces, .stationary,
                            .fullScreenAuxiliary, .ignoresCycle]
panel.isOpaque = false
panel.backgroundColor = .clear
panel.hasShadow = false
Enter fullscreen mode Exit fullscreen mode
  • .nonactivatingPanel means clicking the notch doesn't pull focus away from the app you're typing in. Essential if you ever want to paste into that app.
  • .canJoinAllSpaces + .stationary keep it on every Space without animating along with Mission Control.
  • .ignoresCycle keeps it out of ⌘-backtick window cycling.

The tricky part is mouse pass-through. When the notch is expanded, the panel is bigger than the shape the user sees, and you don't want it swallowing clicks meant for menu bar items next to it. I poll the mouse position every 50 ms while it's open and flip ignoresMouseEvents depending on whether the pointer is over the drawn shape. Crude, but rock solid, and cheaper than it sounds because the timer returns immediately unless the panel is expanded.

3. Clipboard history: there is no "clipboard changed" notification

NSPasteboard has no notification for changes. Every clipboard manager on the Mac polls changeCount:

pollTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { [weak self] _ in
    MainActor.assumeIsolated { self?.poll() }
}

func poll() {
    guard pasteboard.changeCount != lastChangeCount else { return }
    lastChangeCount = pasteboard.changeCount
    // read the new item…
}
Enter fullscreen mode Exit fullscreen mode

0.4 s is a good balance: fast enough that nothing feels missed, cheap enough to be invisible in Activity Monitor.

The part that matters more is what you refuse to record. There's a long-standing convention at nspasteboard.org: apps add marker types to tell clipboard managers how to treat a copy.

static let ignoredTypes: Set<String> = [
    "org.nspasteboard.ConcealedType",     // passwords: never store
    "org.nspasteboard.TransientType",     // only there for a moment
    "org.nspasteboard.AutoGeneratedType", // not copied by the user
    "com.agilebits.onepassword",          // 1Password's own marker
]
Enter fullscreen mode Exit fullscreen mode

If any of these are on the item, skip it. I also exclude password apps by bundle ID (1Password, Bitwarden, Apple Passwords, Keychain Access and friends) because not every app sets the markers, and I run a small on-device detector for things that look like secrets (card numbers, one-time codes, API keys) so they're masked and auto-deleted.

4. The dictation trap

This one came from a user. Dictation apps like Wispr Flow "type" by putting your words on the clipboard, pasting, and then putting back whatever was there before. A naive clipboard manager sees two new copies: the dictation and the restore. Worse: if you'd copied a password just before dictating, the restored copy comes back without its Concealed marker, so it gets saved in plain text.

The fix is a small guard: when a private (Concealed) write is followed quickly by an unmarked write, treat the second one as a restore and don't record it. Getting the timing window right took a test harness that replays the exact sequence of pasteboard writes, which I'd recommend for anything clipboard-related. It's very hard to reason about by hand.

5. Finding a screenshot by what it says

Every image and screenshot in the history goes through Vision on the Mac:

let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
try VNImageRequestHandler(cgImage: image).perform([request])
let text = (request.results ?? []).compactMap { $0.topCandidates(1).first?.string }
Enter fullscreen mode Exit fullscreen mode

Run it off the main thread, store the text next to the clip, and searching "gate 22" finds the boarding pass. It's all on device, which made the privacy story easy: nothing leaves the Mac.

6. Shipping outside the App Store

A notch overlay, global shortcuts and pasting into other apps don't fit the sandbox, so NotchDesk ships as a Developer ID app, notarized by Apple. A few things I learned the hard way:

  • Notarize the DMG, then staple it (xcrun notarytool submit --wait, then xcrun stapler staple) so it opens even offline.
  • Store notary credentials in a keychain profile and check it exists before you start a release build. Mine vanished twice. I'm moving to an App Store Connect API key.
  • Don't send your DMG over WhatsApp. One tester got "NotchDesk can't be opened" with no Open button. The kernel log said the file was quarantined by WhatsApp and "created without user consent", because the chat app saved it automatically. macOS refuses to run those at all, notarized or not. A browser download works fine. I added that to the support page.

Where it's at

NotchDesk launches on Thursday 15 October: meeting countdown and one-click Join (Meet, Zoom, Teams, Webex, FaceTime, Slack huddles), clipboard history with Quick Paste (βŒƒβŒ˜V), a file shelf with AirDrop, and a few small tools in the notch. No account, no analytics. There's a 14-day free trial; the first 100 buyers get a lifetime license.

πŸ‘‰ https://notchdesk.com/?utm_source=devto&utm_medium=social&utm_campaign=launch

If you've built something for the notch, or you've fought NSPasteboard yourself, I'd love to hear how you handled it in the comments.

Disclosure: an AI assistant drafted this post from NotchDesk's real code and my notes, and I used AI coding assistants while building the app. The code snippets and the war stories are from the real project.

Top comments (1)

Collapse
 
kyisaiah47 profile image
kyisaiah47 •

Does the restore guard use the source app as well as timing, or can a legitimate concealed copy immediately followed by another app's copy get discarded?