Cover: 5 small repos, 1 hard lesson each, with five key caps for CGEvent, Speech, AVCapture, .app and Motion

Most Apple tutorials show the API call and stop before the part where your panel steals focus, the framework reports "installed" too early, or the Simulator has no camera feed to give you.

Five of my public GitHub repos each ran into one of those walls. They are small: the largest is under 3,000 lines of Swift, tests included. This post goes through what each one does, the one technique worth taking from it, and the places where the code is weak or incomplete, because those gaps are useful too. I wrote these notes by reading the source at each repo's latest commit and checking the API behavior against Apple's documentation. I did not benchmark anything for this post, so you won't find performance numbers here.

Repo Last commit Size The hard part License stated
mac-keyboard-bar 2026-04-21 1,245 lines of Swift Typing into another app without taking focus README says MIT
mac-live-translate 2026-05-19 2,952 lines of Swift Frameworks that stall or report progress badly README says MIT
SimCameraBrige 2025-10-18 526 lines of Swift, plus a prebuilt Mac app Feeding a Mac webcam into the iOS Simulator None
markdownreader 2025-09-22 1,346 lines of Swift Building a .app with no Xcode project All rights reserved
shake-testing 2025-09-14 316 lines of JavaScript Motion permission in iOS Safari None

The license column matters if you plan to copy code. The two MIT repos state that in the README, but neither has a LICENSE file yet, so read them freely and ask before lifting large parts of anything else on this list.

1. mac-keyboard-bar: type into another app without taking focus

This is a menu bar app that shows a floating on-screen keyboard, so you can type from the couch with only a wireless mouse. The keystrokes have to land in whichever app is frontmost. The moment your keyboard window becomes active, the keys go to the wrong place.

Two things work together to prevent that. The panel is created with the .nonactivatingPanel style, which Apple describes as a panel that does not activate the owning app. The subclass then refuses key and main status outright:

final class FloatingPanel: NSPanel {
    override var canBecomeKey: Bool { false }
    override var canBecomeMain: Bool { false }
}

panel = FloatingPanel(
    contentRect: frame,
    styleMask: [.nonactivatingPanel, .titled, .closable, .fullSizeContentView],
    backing: .buffered,
    defer: false
)
panel.level = .floating
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
panel.hidesOnDeactivate = false

The last three lines cover cases that are easy to forget. Without them, a floating tool can vanish when you change Spaces or when the app goes inactive, and this app is never supposed to be active at all. main.swift sets the activation policy to .accessory and Info.plist carries LSUIElement, so there is no Dock icon either.

The injection itself is a short pair of CGEvent calls, one key down and one key up:

func inject(keyCode: CGKeyCode, flags: CGEventFlags) {
    let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true)
    down?.flags = flags
    poster(down)

    let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false)
    up?.flags = flags
    poster(up)
}

Look at poster. It is a closure injected through init, and it defaults to event?.post(tap: .cghidEventTap). That one seam lets the tests capture events without typing into whatever is on screen during a test run. If you have any code that talks to the system this way, wrap the final side effect in a closure and the rest becomes easy to test.

Modifiers follow the same simple approach. Shift, Command, Option and Control are toggled as state, turned into CGEventFlags when the next key is sent, then cleared by clearOneShot(). That gives you Command+C from a mouse: click Command, click C.

Watch for three things if you build on this:

  • The sandbox is off (ENABLE_APP_SANDBOX = NO), and the README says CGEvent.post requires that. A reply from Apple's Developer Technical Support on the forums is more nuanced: it says AXIsProcessTrustedWithOptions isn't allowed in the sandbox and suggests CGRequestPostEventAccess and CGPreflightPostEventAccess, with a clear warning that the author hadn't confirmed the details. Only App Review can say whether such an app would pass. Plan on direct distribution unless you test that route yourself.
  • The project sets SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor. Several services are marked nonisolated, and the comments explain why: a runtime crash in swift_task_deinitOnExecutorImpl when one was deallocated off the main thread during test teardown. If you adopt that build setting, that comment could save you an afternoon.
  • Each modifier toggle rebuilds the SwiftUI root view through reinstallRootView(). That works for a small keyboard, but an observable state object would be a cleaner design.

2. mac-live-translate: assume the framework can hang

This app listens to spoken Chinese and shows English as people talk. It uses SFSpeechRecognizer with the zh-CN locale and Apple's Translation framework, which needs macOS 15. The interesting code deals with what happens when those frameworks misbehave.

The transcript pipeline is short and worth copying as a pattern. Partial results update the source text at once, translation waits for a 300 ms debounce, and a final result cancels the debounce and translates straight away:

private func handleTranscript(_ text: String, isFinal: Bool) {
    guard text.containsChinese else { return }
    output?.didUpdateSourceText(text)

    if isFinal {
        debounceTask?.cancel()
        translateNow(text, isFinal: true)
    } else {
        scheduleDebouncedTranslate(text)
    }
}

The containsChinese check stops English chatter and room noise from turning into nonsense. Deduplication only applies to partial results, so two identical sentences in a row both reach the history list.

The model download gets more care. A code comment in TranslationInstallVerifier says prepareTranslation() can resolve before the model is really on disk. Apple's documentation fits that: if the languages are already installed or in the middle of downloading, the function returns without prompting. So a successful return doesn't prove the model is ready. The app polls LanguageAvailability once a second, up to 240 times, and after the first few polls it runs a real test translation of 你好 every fifth attempt. The status API and the working result are treated as two separate signals, and either can declare the model ready. Apple's API doesn't report download bytes or percent, so the overlay shows a time-based estimate capped at 95% until the install is confirmed. All of those values live in one TranslationDownloadConfig struct, so tests can inject faster settings.

Two caveats came out of reading this code, and they are the part I'd study hardest.

The first concerns withTimeout, which races the work against a sleep inside withThrowingTaskGroup. Apple's documentation says a group always waits for all child tasks before returning, and that "even canceled tasks must run until completion." So if the real operation ignores cancellation and truly hangs, the timer fires but withTimeout still can't return. The repo's test uses Task.sleep, which responds to cancellation, so it passes and doesn't exercise this case. For a call that may never return, one option is to run it in an unstructured Task, await a continuation that either the work or the timer resumes once, and accept that the stuck task keeps running in the background.

The second is privacy. The README says everything stays on the device. Translation does: Apple's note says all TranslationSession translations are processed on the user's device, although Apple may collect usage metrics that exclude the content. Speech has a conditional in the code:

if recognizer.supportsOnDeviceRecognition {
    request.requiresOnDeviceRecognition = true
}

Apple says requiresOnDeviceRecognition is what keeps audio off the network, and the request only honors it when supportsOnDeviceRecognition is true. On a machine where that value is false, this code carries on without the requirement. If you fork this for sensitive meetings, make that branch stop with an error. Also expect some accuracy loss: Apple says on-device requests won't be as accurate.

The structure is also a good small example of VIPER with protocol-based services. SpeechRecognizing and Translating are mocked in the test target, which has five test suites covering the pipeline and the presenter's state changes.

3. SimCameraBrige: fake a camera in the Simulator

The repo name really is spelled that way. The idea: a Mac app reads the webcam and serves the latest frame as a JPEG at http://localhost:8080/frame, and an iOS helper pulls those frames into your app while it runs in the Simulator.

Two Swift files make up the iOS side. SimBridgeCaptureSession subclasses AVCaptureSession, so existing capture code can keep its shape. A CADisplayLink at 30 fps fetches a frame, and each frame goes through this path:

JPEG data -> UIImage -> CGImage -> CVPixelBuffer -> CMSampleBuffer
          -> captureOutput(_:didOutput:from:) on your existing delegate

Sending a real CMSampleBuffer to your delegate is the clever part, since your frame-processing code doesn't need to know the source changed. Everything sits inside #if targetEnvironment(simulator), and SimCameraPickerController has a separate real-camera path for devices.

The gaps are worth knowing before you try it:

  • The Mac server's source code isn't in the repo. You get the iOS helper files and SimCameraBridge.zip, a prebuilt debug app. Its strings show it listens with NWListener on port 8080. The helper's error message says to run cd macOSApp && swift run, but that folder doesn't exist in the repo, so use the zip and inspect it first, as you would any prebuilt binary.
  • fetchFrame starts a new request on every display tick and doesn't check whether the previous one finished. Under load, frames can pile up or arrive out of order. A simple in-flight flag fixes that.
  • createPixelBuffer makes a new CIContext for each frame. Apple's Core Image performance guide says not to create one every time you render, because contexts hold a lot of state. Store one as a property.
  • Keep the URL as localhost. Apple's App Transport Security notes say that from iOS 17 connections to IP addresses are no longer allowed by default, so switching to 127.0.0.1 could break. Setting NSAllowsLocalNetworking in your debug configuration states the intent clearly.

4. markdownreader: a Mac app with no Xcode project

This is a SwiftUI Markdown reader with a folder sidebar, recent folders and Swift syntax highlighting. Its README credits Claude Code with creating it from my idea, so it's also a fair sample of what that workflow produced in 2025.

The lesson is in Package.swift and build.sh. There is no .xcodeproj. SwiftPM builds a plain executable, then the script assembles the app bundle by hand:

swift build -c release
mkdir -p MarkdownReader.app/Contents/MacOS
mkdir -p MarkdownReader.app/Contents/Resources
cp .build/release/MarkdownReader MarkdownReader.app/Contents/MacOS/

After that, sips resizes one PNG into every icon size, iconutil -c icns builds the icon file, and a heredoc writes Info.plist. Reading these 100-odd lines teaches you what a .app is: a folder with a known layout. CFBundleDocumentTypes registers the app as a Viewer for net.daringfireball.markdown, which is how it can show up for Markdown files. The script doesn't sign or notarize anything, so it's for local use, and you'd add those steps before giving the app to anyone.

The parser in MarkdownParser.swift is hand written and walks the file line by line. For a reader of your own notes that's fine, and it's only 214 lines. Anything that accepts other people's Markdown needs a real CommonMark parser, because this one recognizes a fixed set of line prefixes.

One design choice would break under the sandbox. Recent folders are saved as plain path strings in UserDefaults, up to five, and cleanupInvalidFolders() drops any that no longer exist. That works because this build isn't sandboxed. Apple's guide on sandbox file access explains the alternative: create a bookmark with withSecurityScope, resolve it on the next launch, then call startAccessingSecurityScopedResource() before using the file. If you plan to take a similar app to the Mac App Store, design around bookmarks from the start.

5. shake-testing: motion permission in iOS Safari

This one is a web page, hosted at jerrypm.github.io/shake-testing, that tests shake detection on iPhones and Android phones. It's useful as a device check, and the permission code is the part people get wrong:

if (typeof DeviceMotionEvent.requestPermission === 'function') {
    const permission = await DeviceMotionEvent.requestPermission();
    return permission === 'granted';
}
return true;

MDN documents two requirements for that call: it needs transient activation, so it must come from something like a button click, and it's limited to secure contexts in some or all supporting browsers. Call it on page load and the promise rejects with NotAllowedError. The repo gets this right, because the call sits behind the Start button.

Detection is simple. It sums the change in x, y and z from accelerationIncludingGravity, averages the last 10 samples to reduce noise, and compares that with a threshold of 25, 15 or 8 for low, medium and high sensitivity. A 500 ms lockout stops one shake from counting several times. There's also a spacebar shortcut, so you can test the flow in a simulator with no motion hardware.

There's one real bug, and it's a common one:

window.addEventListener('devicemotion', this.handleDeviceMotion.bind(this));
// later
window.removeEventListener('devicemotion', this.handleDeviceMotion.bind(this));

bind returns a new function each time, so the remove call never matches and the listener stays attached. An isActive check hides the problem while stopped, but every Start adds one more live listener, and each one feeds the same buffer. The fix is to bind once in the constructor:

this.onMotion = this.handleDeviceMotion.bind(this);
window.addEventListener('devicemotion', this.onMotion);
window.removeEventListener('devicemotion', this.onMotion);

Which one to read first

If you're building a Mac utility, start with mac-keyboard-bar for the panel setup and the injected poster seam. If you work with any Apple framework that downloads models, read TranslationInstallVerifier in mac-live-translate, then fix the timeout caveat in your own version. If you've only ever shipped from Xcode, spend ten minutes with build.sh in markdownreader.

The markdownreader README credits Claude Code, and on code like this an agent needs firm instructions about your architecture and what "done" must prove. If you use Claude Code on iOS projects, I sell Claude Code + Xcode: The Production iOS Workflow Kit on Gumroad. It's a copy-ready Claude Code setup for SwiftUI and UIKit with 6 commands, 10 agents, 8 rules and a runnable Xcode sample.

Sources