This note records the native macOS development workflow that now works on this machine for SwiftUI/Xcode app work and closed-loop UI automation.
- Xcode
26.3 - selected developer directory:
/Applications/Xcode.app/Contents/Developer - Appium
3.2.2 - Appium
mac2driver3.2.16 xcodesaria2swiftformatswiftlintxcbeautifyAppium Inspector
This enables a much tighter closed loop for native macOS app development:
- write SwiftUI code
- build from the terminal with
xcodebuild - drive the real app with XCTest-backed Appium
mac2 - inspect the accessibility tree
- take screenshots
- click native controls programmatically
This is especially useful when working on a macOS desktop app where browser-only tooling would miss native behavior and layout.
The following validations succeeded:
xcode-select -ppoints at full Xcode, not Command Line Toolsxcodebuild -versionreports Xcode26.3appium driver doctor mac2passes required checks- Appium server starts with the
mac2driver - a real Appium
mac2session was created againstcom.apple.TextEdit - the session successfully returned:
- the accessibility tree via
GET /source - a full screenshot via
GET /screenshot - a successful native click command via
POST /element/.../click
- the accessibility tree via
Verify the toolchain:
xcode-select -p
xcodebuild -version
xcrun swift --version
appium --version
appium driver list --installed
appium driver doctor mac2
swiftformat --version
swiftlint version
xcbeautify --versionStart Appium:
appium --use-drivers=mac2 --address 127.0.0.1 --port 4723 --base-path /wd/hubCreate a test session against a macOS app:
curl -X POST http://127.0.0.1:4723/wd/hub/session \
-H 'Content-Type: application/json' \
-d '{
"capabilities": {
"alwaysMatch": {
"platformName": "Mac",
"appium:automationName": "Mac2",
"appium:bundleId": "com.apple.TextEdit",
"appium:showServerLogs": true
},
"firstMatch": [{}]
}
}'Useful follow-up commands:
curl http://127.0.0.1:4723/wd/hub/session/<session-id>/source
curl http://127.0.0.1:4723/wd/hub/session/<session-id>/screenshot
curl -X DELETE http://127.0.0.1:4723/wd/hub/session/<session-id>For native app projects, the best loop on this machine is:
- Build the app with mock data and deterministic launch modes.
- Give every meaningful UI element an
accessibilityIdentifier. - Use launch arguments or environment variables to boot the app into specific states.
- Run small, high-value automation checks with XCUITest or Appium
mac2. - Use screenshots and the accessibility tree to verify the actual UI state.
- Check for fresh macOS problem reports after launches and smoke runs.
This is more reliable than trying to infer native UI behavior from code alone.
- add
accessibilityIdentifierto every important button, segment, drawer, card, and modal - add a mock app state mode for deterministic UI previews and test launches
- keep one or two canned archive/source states for fast visual testing
- format with
swiftformat - lint with
swiftlint - pipe
xcodebuildoutput throughxcbeautify
These are the highest-leverage things to build into the app itself from day one:
- a
MockAppStatelayer that can boot the full UI without real backend or device connections - launch arguments for named states such as:
- all books dark
- mixed two-week coverage
- regeneration in progress
- new source awaiting inventory decision
accessibilityIdentifiervalues on every meaningful interactive element- stable identifiers on segment blocks, source books, drawers, dialogs, and primary actions
- a small automation-friendly diagnostics surface that can reveal the current mock state when needed
- a way to force-disable animations for deterministic screenshot capture
Recommended identifier shape:
timeline.segment.mar10_11sourceBook.goprosourceBook.phoneCapturedrawer.segmentDetailbutton.importDevicesbutton.addToInventorybutton.swapInventoryItem
Recommended launch arguments:
--mock-state mixed-coverage--mock-state all-dark--mock-state regeneration--mock-state new-source--disable-animations--ui-testing
Recommended code-level pattern:
- Keep domain state in plain Swift structs that can be constructed in tests and previews.
- Make the app root read launch arguments and choose a mock or live dependency container.
- Build SwiftUI previews from the same mock states used by Appium/XCUITest.
- Prefer deterministic timers/state transitions over real clocks in test mode.
This keeps the app easy to drive by code, by previews, and by native automation tools without maintaining separate fake implementations for each.
Example:
xcodebuild test -scheme Guardian -destination 'platform=macOS' | xcbeautifyAn important missing loop in native app work is detecting fresh macOS crash/problem reports automatically instead of waiting for a human to notice the report dialog.
On this machine, a practical default is:
- launch the app in a deterministic mock state
- capture a screenshot
- inspect
~/Library/Logs/DiagnosticReports - fail the smoke run if a new
.ips,.crash, or similar problem report appears for the app
Why this works:
- Apple’s Console and analytics/crash-report flow treats these reports as the local source of truth for unexpected quits
- this catches app failures even when the UI briefly appears and then dies
- it closes the loop between “app launched” and “app actually survived launch cleanly”
Recommended repo-level pattern:
- Record a
sincetimestamp before launch. - Launch the app in mock UI-testing mode.
- Wait briefly for startup.
- Capture a screenshot.
- Scan
~/Library/Logs/DiagnosticReportsfor reports newer than the timestamp matching the app process or bundle identifier. - Fail immediately if a fresh report exists.
Recommended local commands inside a repo:
just app-build
just app-smokeThe smoke command should eventually do all of the following:
- open the app
- use deterministic launch arguments
- capture a screenshot artifact
- optionally query the accessibility tree
- detect new problem reports
- stop the app cleanly
This gets native macOS work much closer to a true closed loop.
Desktop screenshots alone are not enough to prove a native app is actually visible. A stronger pattern is:
- launch the app
- ask macOS whether the bundle is running
- ask whether it owns an on-screen window
- capture that specific window by window ID
- only then trust the screenshot artifact
The key macOS APIs are:
NSRunningApplication.runningApplications(withBundleIdentifier:)NSWorkspace.shared.frontmostApplicationCGWindowListCopyWindowInfo
A practical repo-local helper can output JSON like:
isRunningisFrontmostvisibleWindowCountmainWindowID- visible window bounds and titles
Then the smoke script can capture the actual app window:
screencapture -x -l "$WINDOW_ID" artifact.pnginstead of a full desktop screenshot.
This closes an important gap: it distinguishes
- process exists
- app is frontmost
- app really has a visible window
which are not the same thing.
- Prefer window-specific screenshots over desktop screenshots whenever possible.
- Prefer stable accessibility IDs on containers and avoid reusing the same identifier on both a title and its parent card.
- If Appium
mac2is available, use it to verify both:- the accessibility tree
- element-level screenshots for specific controls
That combination gives a much better answer to "is the app truly open and showing the thing I think it is?" than process checks alone.
When a native desktop app is under active development, do not rely on a generic desktop screenshot to answer "is the app open?" Use a structured window-state probe instead.
On this machine, GuardianDesktop uses:
scripts/app_window_state.swift --bundle-id com.hapticasensorics.guardiandesktop --process GuardianDesktopThat probe should report at least:
isRunningisFrontmostvisibleWindowCountisInteractablemainWindowIDmatchedBy
The smoke path should then use mainWindowID for the capture so the proof artifact is the app window itself, not the whole desktop.
Important caveat:
isInteractabledepends on Accessibility trust, so the shell or helper process may need permission before that signal is reliable.- In active development, some macOS app launches do not register cleanly by bundle ID even when a real window is visible.
- The probe should therefore fall back to process-name matching,
pgrep, and window-owner matching rather than treating bundle lookup as the only source of truth. - If
matchedByreports one of those fallback paths and the window is visible/interactable, agents should treat that as a real open-app state.
appium-mac2-driveris built on Apple XCTest.xcode-selectmust point at full Xcode.Xcode Helper.appmay need Accessibility permission in some environments.appium driver doctor mac2is the quickest sanity check for prerequisites.automationmodetool enable-automationmode-without-authenticationis an optional hardening step mentioned by the driver doctor for reducing Automation Mode prompts.
When native macOS behavior matters, prefer:
- SwiftUI for app code
xcodebuildfor repeatable terminal builds and tests- XCUITest for first-party native UI coverage
- Appium
mac2for extra closed-loop desktop automation and inspection
This gives coding agents a much tighter native iteration loop than relying on browser-only workflows.