Skip to content

Latest commit

 

History

History
303 lines (222 loc) · 9.89 KB

File metadata and controls

303 lines (222 loc) · 9.89 KB

Swift + Xcode Agent Workflow

This note records the native macOS development workflow that now works on this machine for SwiftUI/Xcode app work and closed-loop UI automation.

Installed Native Stack

  • Xcode 26.3
  • selected developer directory: /Applications/Xcode.app/Contents/Developer
  • Appium 3.2.2
  • Appium mac2 driver 3.2.16
  • xcodes
  • aria2
  • swiftformat
  • swiftlint
  • xcbeautify
  • Appium Inspector

Why This Stack Matters

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.

Verified Locally

The following validations succeeded:

  • xcode-select -p points at full Xcode, not Command Line Tools
  • xcodebuild -version reports Xcode 26.3
  • appium driver doctor mac2 passes required checks
  • Appium server starts with the mac2 driver
  • a real Appium mac2 session was created against com.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

Core Commands

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 --version

Start Appium:

appium --use-drivers=mac2 --address 127.0.0.1 --port 4723 --base-path /wd/hub

Create 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>

Recommended Development Pattern

For native app projects, the best loop on this machine is:

  1. Build the app with mock data and deterministic launch modes.
  2. Give every meaningful UI element an accessibilityIdentifier.
  3. Use launch arguments or environment variables to boot the app into specific states.
  4. Run small, high-value automation checks with XCUITest or Appium mac2.
  5. Use screenshots and the accessibility tree to verify the actual UI state.
  6. 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.

Strong Defaults For New SwiftUI Apps

  • add accessibilityIdentifier to 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 xcodebuild output through xcbeautify

Inside The App

These are the highest-leverage things to build into the app itself from day one:

  • a MockAppState layer 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
  • accessibilityIdentifier values 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_11
  • sourceBook.gopro
  • sourceBook.phoneCapture
  • drawer.segmentDetail
  • button.importDevices
  • button.addToInventory
  • button.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:

  1. Keep domain state in plain Swift structs that can be constructed in tests and previews.
  2. Make the app root read launch arguments and choose a mock or live dependency container.
  3. Build SwiftUI previews from the same mock states used by Appium/XCUITest.
  4. 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' | xcbeautify

Problem Report Detection

An 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:

  1. Record a since timestamp before launch.
  2. Launch the app in mock UI-testing mode.
  3. Wait briefly for startup.
  4. Capture a screenshot.
  5. Scan ~/Library/Logs/DiagnosticReports for reports newer than the timestamp matching the app process or bundle identifier.
  6. Fail immediately if a fresh report exists.

Recommended local commands inside a repo:

just app-build
just app-smoke

The 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.

Reliable App-Open Detection

Desktop screenshots alone are not enough to prove a native app is actually visible. A stronger pattern is:

  1. launch the app
  2. ask macOS whether the bundle is running
  3. ask whether it owns an on-screen window
  4. capture that specific window by window ID
  5. only then trust the screenshot artifact

The key macOS APIs are:

  • NSRunningApplication.runningApplications(withBundleIdentifier:)
  • NSWorkspace.shared.frontmostApplication
  • CGWindowListCopyWindowInfo

A practical repo-local helper can output JSON like:

  • isRunning
  • isFrontmost
  • visibleWindowCount
  • mainWindowID
  • visible window bounds and titles

Then the smoke script can capture the actual app window:

screencapture -x -l "$WINDOW_ID" artifact.png

instead 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.

Practical Native GUI Capture Notes

  • 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 mac2 is 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.

App State Probe

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 GuardianDesktop

That probe should report at least:

  • isRunning
  • isFrontmost
  • visibleWindowCount
  • isInteractable
  • mainWindowID
  • matchedBy

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:

  • isInteractable depends 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 matchedBy reports one of those fallback paths and the window is visible/interactable, agents should treat that as a real open-app state.

Notes About Permissions

  • appium-mac2-driver is built on Apple XCTest.
  • xcode-select must point at full Xcode.
  • Xcode Helper.app may need Accessibility permission in some environments.
  • appium driver doctor mac2 is the quickest sanity check for prerequisites.
  • automationmodetool enable-automationmode-without-authentication is an optional hardening step mentioned by the driver doctor for reducing Automation Mode prompts.

Practical Recommendation

When native macOS behavior matters, prefer:

  • SwiftUI for app code
  • xcodebuild for repeatable terminal builds and tests
  • XCUITest for first-party native UI coverage
  • Appium mac2 for extra closed-loop desktop automation and inspection

This gives coding agents a much tighter native iteration loop than relying on browser-only workflows.