- 01.Starting Point
- 02.Built the Menu Bar UI
- 03.ForEach Duplicate ID Bug
- 04.Port Number Formatting Bug
- 05.SwiftUI Animation Didn't Work
- 06.Window Focus Issue
- 07.Onboarding Reset
- 08.Commit
- 09.TestFlight Attempt #1: App Store Connect
- 10.Pivot to Direct Distribution
- 11.Getting Developer ID Certificate
- 12.Signing etserver and dylibs
- 13.Notarization Submitted
- 14.Distribution Site Setup
- 15.Where We Landed
- 16.Takeaways
PocketMac Menu Bar UI and Distribution Journey
PocketMac v2 onboarding was complete from a previous session — Welcome → Explanation → System Check → Pairing → Success flow. The app worked but had a minimal menu bar (just \"Setup Assistant...\" and \"
Starting Point
PocketMac v2 onboarding was complete from a previous session — Welcome → Explanation → System Check → Pairing → Success flow. The app worked but had a minimal menu bar (just "Setup Assistant..." and "Quit"). Goal: build a proper menu bar experience and get to TestFlight.
Built the Menu Bar UI
Created PocketMac/PocketMac/MenuBar/MenuBarView.swift with:
- Server toggle (Tailscale-style switch instead of Start/Stop buttons)
- Inline pairing mode with 6-digit code display
- Countdown timer when pairing
- Hover states on menu rows
- Settings and Setup Assistant links
Key design decisions from user feedback:
- No keyboard shortcuts — user said "we don't need shortcuts right now"
- Toggle instead of red "Stop" button — "Stop" color was "too saturated red, looks like an error"
- Normal button for pairing — original was orange/branded, user wanted it plain
ForEach Duplicate ID Bug
Hit this warning twice:
ForEach<Array<Character>, Character, ...>: the ID 5 occurs multiple times within the collection
Cause: Using ForEach(Array(code), id: \.self) on a pairing code like "195655" — two 5s means duplicate IDs.
Fix: Use enumerated index:
ForEach(Array(code.enumerated()), id: \.offset) { _, digit in
Text(String(digit))
// ...
}Fixed in both MenuBarView.swift and PairingStep.swift.
Port Number Formatting Bug
Port showed as "2,022" instead of "2022":
Text("Port \(port)") // Uses locale number formattingFix:
Text("Port \(String(port))") // Explicit string conversionSwiftUI Animation Didn't Work
The iPhone→Mac connection animation in ExplanationStep.swift was static. Original code:
private struct ConnectionArrow: View {
@State private var phase: CGFloat = 0
var body: some View {
HStack(spacing: 6) {
ForEach(0..<4, id: \.self) { i in
Circle()
.opacity(opacity(for: i)) // Computed from phase
}
}
.onAppear {
withAnimation(.linear(duration: 1.2).repeatForever(autoreverses: false)) {
phase = 4
}
}
}
private func opacity(for index: Int) -> Double {
let position = (phase + Double(index)).truncatingRemainder(dividingBy: 4)
return position < 1 ? 1.0 : 0.3
}
}Why it didn't animate: SwiftUI doesn't re-evaluate opacity(for:) on each animation frame because it's a computed function, not a direct dependency on the animated @State.
Fix: Each element needs its own animation modifier:
private struct ConnectionArrow: View {
@State private var isAnimating = false
var body: some View {
HStack(spacing: 6) {
ForEach(0..<4, id: \.self) { i in
Circle()
.fill(Color.pocketAccent)
.frame(width: 6, height: 6)
.opacity(isAnimating ? 1.0 : 0.3)
.animation(
.easeInOut(duration: 0.4)
.repeatForever(autoreverses: true)
.delay(Double(i) * 0.15),
value: isAnimating
)
}
}
.onAppear {
isAnimating = true
}
}
}The key insight: animation is tied to actual state change (isAnimating), and each dot has its own .animation() modifier with staggered .delay().
Window Focus Issue
Windows opened from menu bar appeared behind other apps because the app is LSUIElement = true (no dock icon).
Fix: Add NSApp.activate() after opening:
MenuRowButton(title: "Settings...") {
openWindow(id: "settings")
NSApp.activate(ignoringOtherApps: true)
}Onboarding Reset
Opening Setup Assistant showed the last step from previous run.
Fix: Reset to welcome on appear:
.onAppear {
step = .welcome
}Commit
93e90f2 feat: Add menu bar UI and settings window for PocketMac
TestFlight Attempt #1: App Store Connect
Tried uploading to App Store for TestFlight. Hit multiple validation errors:
Invalid Code Signing Entitlements. key 'com.apple.security.files.home-relative-path.read-write' is not supported.
The Info.plist must contain a LSApplicationCategoryType key.
App sandbox not enabled. The following executables must include the "com.apple.security.app-sandbox" entitlement:
- PocketMac.app/Contents/MacOS/PocketMac
- PocketMac.app/Contents/Resources/etserver_dist/etserver
Reality check: App Store requires full sandboxing. PocketMac needs to:
- Write to
~/.ssh/authorized_keys - Run bundled
etserveron port 2022 - Access files outside sandbox
This is why iTerm2, Docker Desktop, Homebrew are NOT on Mac App Store.
Pivot to Direct Distribution
Switched to Developer ID + notarization for direct distribution (like most developer tools).
First attempt:
"etserver" must be rebuilt with support for the Hardened Runtime.
The bundled etserver binary wasn't signed with hardened runtime.
Getting Developer ID Certificate
Only had "Apple Development" certificate:
$ security find-identity -v -p codesigning
1) E01277AC6FC4E9A751ECD241D9EDAB3E438EDB79 "Apple Development: pk.joel@gmail.com (FUJG77L97Z)"Needed "Developer ID Application" for notarized distribution.
Process:
- Apple Developer portal → Certificates → + → Developer ID Application
- Choose "G2 Sub-CA (Xcode 11.4.1 or later)" — modern option
- Create CSR in Keychain Access (CA Email can be left blank)
- Upload CSR, download cert, double-click to install
Got error -25294 on import — this means private key mismatch. User had to redo the CSR on the same machine where they'd import the cert.
After retry:
$ security find-identity -v -p codesigning
1) E01277AC6FC4E9A751ECD241D9EDAB3E438EDB79 "Apple Development: pk.joel@gmail.com (FUJG77L97Z)"
2) 142F087DFF15B5EA14B4F52F97DA1CB5E93C18E3 "Developer ID Application: Joel Brubaker (UQ27DB7N8K)"Signing etserver and dylibs
etserver was only ad-hoc signed:
$ codesign -dv --verbose=4 etserver_dist/etserver 2>&1 | grep Signature
Signature=adhocSigned everything with Developer ID + Hardened Runtime:
IDENTITY="Developer ID Application: Joel Brubaker (UQ27DB7N8K)"
ETSERVER_DIR="/Users/joel/Code/pocket/PocketMac/etserver_dist"
# Sign all dylibs first
for lib in "$ETSERVER_DIR"/libs/*.dylib; do
codesign --force --options runtime --timestamp --sign "$IDENTITY" "$lib"
done
# Sign etserver
codesign --force --options runtime --timestamp --sign "$IDENTITY" "$ETSERVER_DIR/etserver"83 files signed. Verified:
$ codesign -dv etserver 2>&1 | grep -E "(Identifier|TeamIdentifier|Runtime)"
Identifier=etserver
TeamIdentifier=UQ27DB7N8K
Runtime Version=15.2.0Runtime Version=15.2.0 confirms Hardened Runtime is enabled.
Notarization Submitted
After clean build + archive, notarization was submitted via Xcode Organizer → Distribute → Developer ID → Upload.
Waiting for Apple (typically 5-15 minutes).
Distribution Site Setup
While waiting, set up download infrastructure.
Created /Users/joel/Code/pocket-site/public/downloads/ for hosting the ZIP.
Created download page at src/app/(main)/download/page.tsx with:
- iOS TestFlight section (placeholder link)
- Mac direct download section (points to
/downloads/PocketMac.zip) - Setup instructions
Updated Hero and CallToAction buttons to point to /download.
Build verified:
$ pnpm build
Route (app) Size First Load JS
├ ○ /download 162 B 106 kBWhere We Landed
Menu bar UI: Complete with toggle, inline pairing, hover states Settings window: General tab (auto-start, status) + Logs tab Onboarding: Resets properly, animations work Distribution: Notarization pending, download page ready
Commits:
93e90f2- Menu bar UI and settings window
Once notarization completes:
- Export from Xcode Organizer
zip -r PocketMac.zip PocketMac.app- Copy to
pocket-site/public/downloads/ - Update TestFlight link in download page
vercel --prod
Takeaways
SwiftUI animation gotcha: Animations based on computed properties from @State don't work. Each view element needs its own .animation() modifier tied directly to the state value.
Mac App Store vs Direct Distribution: If your app needs to write outside its sandbox (like ~/.ssh/), run helper binaries, or listen on privileged ports — App Store isn't an option. Most developer tools use notarized direct distribution.
Hardened Runtime for bundled binaries: Any executable in your app bundle must be signed with --options runtime for notarization. Sign dylibs first, then the main binary.
Developer ID vs Apple Development: Different certificates for different purposes. Apple Development = TestFlight/device testing. Developer ID Application = notarized direct distribution.