
LLDB is a full command-line debugger and does not need the Xcode GUI to attach to a process, set breakpoints, or evaluate Swift expressions. On a rented Mac that you reach through a terminal, LLDB is the primary interface. This guide drives a complete debugging session from one shell: build, launch paused, attach by name, inspect, and save aliases.
Run the commands on the remote Mac itself. On Figment's virtual Intel macOS ($29/month), open the browser-console desktop terminal; inbound SSH to that VM is not exposed today. On an M-series Mac (from $95/month), confirm SSH access in the configuration quote and the commands below work over ssh -t as written.
The iOS Simulator runs each app as a regular macOS process owned by your user account, so LLDB attaches with the same mechanisms it uses for any user process: by PID or by process name. No daemon, port forwarding, or extra entitlement is required.
Two attach strategies matter for a headless session:
lldb -n MyApp --waitfor): LLDB blocks until a process with that name launches, then stops it at the first instruction it can.lldb -p 12345): Useful when the app is already running. Find the PID with pgrep -f MyApp.app or from xcrun simctl launch output.Combined with xcrun simctl launch --wait-for-debugger, this gives a deterministic "launch paused, attach, continue" loop without the Xcode GUI.
The sequence below uses a sample scheme and bundle ID — replace MyApp, MyAppScheme, and com.example.MyApp with your project's actual values.
Build a debug configuration and capture the simulator .app:
cd ~/MyApp
set -euo pipefail
xcodebuild \
-scheme MyAppScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-configuration Debug \
-derivedDataPath /tmp/MyApp-derived \
build
APP_PATH=$(find /tmp/MyApp-derived/Build/Products/Debug-iphonesimulator \
-maxdepth 2 -name 'MyApp.app' -not -path '*Tests*' | head -1)
echo "APP_PATH=$APP_PATH"
set -euo pipefail makes the script stop on the first failing command, so a failed build never falls through to install or launch.
Boot the simulator and install the app:
xcrun simctl boot 'iPhone 16' 2>/dev/null || true
xcrun simctl install booted "$APP_PATH"
Launch the app paused, waiting for a debugger to attach:
xcrun simctl launch --wait-for-debugger --console-pty booted com.example.MyApp
# Output example:
# com.example.MyApp: 48123
The PID is the number after the colon. Open a second shell on the same Mac and attach:
lldb -p 48123
Or, if you prefer attach-by-name so you can relaunch without copying a PID:
# Shell A on the Mac
lldb -n MyApp --waitfor
# Shell B on the Mac
xcrun simctl launch --wait-for-debugger booted com.example.MyApp
Once attached, LLDB sits at the first user frame. continue resumes execution; your breakpoints take over from there.
LLDB's breakpoint set accepts file-and-line, function name, and Objective-C selector forms:
(lldb) breakpoint set --file ContentView.swift --line 42
(lldb) breakpoint set --name viewDidLoad
(lldb) breakpoint set --selector dealloc
(lldb) breakpoint list
(lldb) breakpoint disable 1
(lldb) continue
thread step-over, thread step-in, and thread step-out move through a stopped frame. The aliases later in this guide shorten them to n, s, and f.
frame variable and expressionAt a stop, frame variable enumerates locals and arguments in the current frame:
(lldb) frame variable # all locals, current values
(lldb) frame variable self # self and its stored properties
(lldb) frame variable -T # add type annotations
For Swift value printing, po runs the debug description and expression evaluates arbitrary Swift in the current frame:
(lldb) po self.viewModel
(lldb) po self.viewModel.items.count
(lldb) expression self.viewModel.items.count
(lldb) expression var $copy = self.viewModel; $copy.reset(); po $copy
If Swift type checking fails on complex generics — a recurring annoyance — fall back to p for a simpler value print or frame variable for a non-evaluated dump.
.lldbinit for Headless SessionsLLDB reads ~/.lldbinit on the Mac at startup. These aliases map to the commands you actually type during a terminal session:
# ~/.lldbinit on the remote Mac
command alias vt frame variable -T
command alias vs frame variable self
command alias dump expression -O --
command alias wp watchpoint set variable
command alias bts thread backtrace --count 20
command alias n thread step-over
command alias s thread step-in
command alias f thread step-out
Confirm the file loaded after restarting LLDB:
(lldb) help vt
# Current executable set to 'Alias for: frame variable -T'
A watchpoint on a property setter is a strong pattern for "who is writing this value":
(lldb) breakpoint set --name viewDidLoad
(lldb) continue
# ... hits viewDidLoad ...
(lldb) wp self.count
# Watchpoint created; LLDB stops on the next write to self.count
(lldb) continue
Pick by the toolchain, not by price alone. Our $29/month plan is a virtual Intel macOS environment (2 vCPU, 8 GiB RAM, 80 GiB disk) running under Linux/KVM — real macOS, no GPU acceleration or audio, no inbound SSH today. It runs Intel-compatible Xcode toolchains, which Apple documents as Xcode 26 or earlier, and that is enough for the LLDB workflow above when you drive it from the browser-console terminal. It is the lowest entry price; it is not an M-series Mac and is not equivalent hardware to an M4.
For SSH-driven sessions as written, newer Xcode on Apple silicon, or visionOS targets, request an M-series Mac (from $95/month) and confirm the exact chip, RAM, disk, and SSH access in the quote. If you want the signing, CI wiring, backups, and monitoring handled, Managed Build Mac is $249/month (one M-series Mac, 30 minutes of monthly support) and Managed Mac Team is $999/month (two M-series Macs, two CI runners, one hour of monthly support).
Request a configuration at /mac-hosting or begin at /mac-hosting/start.
Related guides: