When I first set out to build ZenLock, I knew I wanted something small and sleek: a menu-bar app that would help me lock in focus sessions, automatically toggle Do Not Disturb, and unobtrusively live in my Mac’s status bar. Over the course of this project, I tackled everything from SwiftUI UI design to shell scripting, debugged linker errors, and even wrestled with macOS privacy settings. In this post, I’ll share my step-by-step process, the hurdles I encountered, and the solutions I applied to arrive at a polished, nimble productivity companion.
Project Setup & Initial Goals
At the very beginning, I had two core goals:
Timer functionality with customizable durations (presets and custom input).
Integration with macOS Focus/Do Not Disturb, so enabling a session would silence notifications automatically. ( could not apply it, faced lots of problems )
I created a new macOS SwiftUI App project in Xcode named ZenLock, added an AppDelegate to handle notifications and permissions, and set up a MenuBarExtra scene in ZenLockApp.swift
. This gave me a base status-bar icon and blank pop-up.
Initial folder structure:
ZenLock
├ AppDelegate.swift
├ ZenLockApp.swift
├ ContentView.swift
├ Info.plist
└ Assets.xcassets/
Building the Timer UI in SwiftUI
Adding Preset Buttons
I wanted three quick presets—5, 10, and 15 minutes—and a clean countdown display. SwiftUI made the layout straightforward:
1HStack(spacing: 12) {
2 presetButton(minutes: 5)
3 presetButton(minutes: 10)
4 presetButton(minutes: 15)
5}
With a helper:
1private func presetButton(minutes: Int) -> some View {
2 Button { selectPreset(minutes * 60) } label: {
3 Label("\(minutes)", systemImage: "timer")
4 }
5}
Custom Time Input (MM:SS)
Next, I needed custom entry in MM:SS
format (or single seconds). I added a TextField
bound to @State private var customTime: String
and a method to parse:
1TextField("MM:SS", text: $customTime)
2 .frame(width: 60)
3 .onSubmit { setCustomTime() }
And in setCustomTime()
, I split by ":"
, converted to minutes and seconds, and reset the timer.
Initially I went with just adding a number and it would update the minutes value to any which user gives, but then to test the applications and for my use case, I wanted seconds to be added too, hence went with this approach.
Countdown Logic with Combine
Using Timer.publish(every: 1, on: .main, in: .common).autoconnect()
, I decremented @State private var remaining: Int
and displayed it in a monospaced font:
1.onReceive(timer) { _ in
2 guard timerRunning else { return }
3 if remaining > 0 { remaining -= 1 } else { finishSession() }
4}
This formed the core timer UI.
Sound Effects & Built-In macOS Sounds
Rather than embedding MP3s, I tapped into macOS’s built-in library, so thats its easier for me to just use sounds rather than managing a assets folder:
1NSSound(named: .init("Ping"))?.play() // session start
2NSSound(named: .init("Funk"))?.play() // session end
This gave a satisfying “ting” and alarm without extra assets.
Do Not Disturb Integration
AppleScript Attempts
My first approach was AppleScript:
1let script = "tell application \"System Events\" to set doNotDisturb to true"
2NSAppleScript(source: script)!.executeAndReturnError(&error)
But I ran into frequent errors such as:
Application isn’t running.
AppleScript was brittle and blocking the UI. I did not understand why I could not link my custom focus to the app.
I did try up looking the docs, but did not go deeper.

Shell Defaults & NotificationCenter Reload
Switching to writing com.apple.notificationcenterui
defaults:
1Process.launched(...) // defaults write doNotDisturb true
2Process.launched(...) // killall NotificationCenter
I was not able to test this in build mode, as it did not trigger DND for my, maybe some mistakes I was doing, or I guess it will work when its deployed ( less likely to be the case ). Anyway lets keep this feature pinned away for the moment till we have a solid solution.
Global Keyboard Shortcut
I wanted ⌘⇧T to toggle ZenLock. I imported the KeyboardShortcuts SPM:
File → Add Packages… →
https://github.com/sindresorhus/KeyboardShortcuts
Created
KeyboardShortcutsController
to listen for.toggleTimer
.Registered a default in
ZenLockApp.init()
:
1if .toggleTimer.shortcut == nil {
2 .toggleTimer.shortcut = .init(.t, modifiers: [.command, .shift])
3}
4
But I could not run the timer with the shortcut, to allow shortcuts from the app I wanted to have the accessibility permissions. To avoid instructing users manually, I prompted for Accessibility at launch:
1_ = AXIsProcessTrustedWithOptions([
2 kAXTrustedCheckOptionPrompt: true
3] as CFDictionary)
This triggered the system dialog, so users could grant control permissions.
App Intents & Native Focus Filters
My early AppleScript enumeration of focus filters failed on newer macOS versions. Then I tried App Intents framework:
Added an App Intents Extension with
ZenLockFocusFilter.swift
conforming toSetFocusFilterIntent
.Marked its
@Parameter var focusName: String?
optional (required by the protocol).In
perform()
, posted:
1NotificationCenter.default.post(
2 name: .zenLockFocusChanged,
3 object: focusName
4)
In
ContentView
, I call:
1let intent = try await ZenLockFocusFilter.current
2currentFocus = intent.focusName ?? "None"
and listen for the notification to update live.
This still not give me first-class, future-proof integration with the system’s Focus feature—still need to further investigate this.
Settings Window & Keyboard Shortcut Editor
Rather than embedding settings in the menu pop-up, I created a standalone SettingsView.swift with:
A TabView: an About tab (“Made with ❤️ by Montek”) and a Shortcuts tab with
KeyboardShortcuts.Recorder(for: .toggleTimer)
.In
ZenLockApp.swift
, I defined:
1MenuBarExtra { ContentView() }
2Window("Settings", id: "Settings") { SettingsView() }
Clicking the gear icon (.overlay
in ContentView
) now calls openWindow(id: "Settings")
to bring up that window.
Hiding the Dock Icon
I wanted ZenLock completely headless—no Dock icon or Cmd-Tab entry. In the image below you see this dock icon, i didn’t want that

After trying setActivationPolicy(.accessory)
in code, I settled on adding this key to Info.plist:
1<key>LSUIElement</key>
2<true/>
In Xcode’s Info tab under Custom macOS Application Target Properties, I clicked “+”, entered Application is agent (UIElement), set the type to Boolean, and checked YES. This made ZenLock a pure menu-bar agent.

Final Project Structure
ZenLock
├── ZenLock.xcodeproj
├── ZenLock
│ ├── AppDelegate.swift // notifications & Accessibility prompt
│ ├── ZenLockApp.swift // @main, scenes, default shortcut
│ ├── ContentView.swift // timer UI + gear overlay
│ ├── SettingsView.swift // About & Shortcuts tabs
│ ├── DoNotDisturbManager.swift
│ ├── TimerManager.swift // DispatchSourceTimer + notifications
│ ├── Info.plist // LSUIElement = YES
│ └── Assets.xcassets
└── ZenLockFocusIntent // App Intents extension
└──Info.plist
Challenges & Lessons Learned
AppleScript vs. shell: AppleScript was unreliable;
defaults
+killall
was also not working.Blocking calls: moved script and preferences writes off the main thread to avoid UI jank.
Focus enumeration: App Intents is the official, supported API—ditch custom AppleScript/CFPreferences hacks.
Accessibility: proactively prompt so global shortcuts just work.
LSUIElement: essential for a menu-bar-only experience.
Every barrier—from linker errors to missing modules—taught me more about macOS internals. I hope this walkthrough helps you build your own menu-bar tools, whether for focus, timers, or other utilities.
Github repo: https://github.com/Montekkundan/ZenLock
For a similar tutorial you can watch this youtube video.