Track UI Events and Network Activity in macOS Using Rust + SwiftUI

Most macOS apps are black boxes. You click a button, something happens—maybe a network request, maybe some system interaction—but it's hard to know what unless you own the source code.
So I built a tool to watch.
This post walks through a hybrid SwiftUI + Rust project that logs every button click and tracks network traffic tied to the clicked app. It combines:
A SwiftUI frontend to simulate normal GUI interaction
A Rust backend using
CGEventTap
and the macOS Accessibility APIPer-process network inspection using the undocumented but powerful
nettop
No kernel extensions. No root. Just clever usage of system APIs and a bit of FFI.
The full source code is available here.
Why build this?
Because reverse-engineering app behavior shouldn't require a debugger or Wireshark session. I wanted to:
Trace which UI elements trigger which network activity
Understand what apps do in response to input
Have a working reference for macOS Accessibility APIs in Rust
The Windows version of this tool relied on Win32 hooks and low-level TCP inspection via GetExtendedTcpTable. You can read that walkthrough here →
This macOS build takes the same spirit to Apple's ecosystem—same goals, different APIs.
Demo: From Click to Packet
When a user clicks Button A, here's what gets logged:
📡 example-mac-app.47727 ↑ 6092 B ↓ 0 B (Δ ↑ 6092 ↓ 0)
[INFO] Button Clicked: App='example-mac-app', PID=47727, ID='ButtonA', Label='Button A'
You get:
The app name
The PID
The element's accessibility label and identifier
A delta of bytes sent/received from that process (captured via
nettop
)
Architecture Overview
+----------------------+ +-----------------------+
| SwiftUI Mac App | ---> | Rust macos-watcher |
| (3 Buttons + Network)| | (Event Tap + nettop) |
+----------------------+ +-----------------------+
⬑ Accessibility API (AXUIElement)
⬐ CGEventTap mouse/keyboard
Frontend: SwiftUI Playground
The Xcode app has a simple interface with three buttons (ButtonA
, ButtonB
, ButtonC
). When clicked, each makes a network request using URLSession
.
Button("Button A") {
fetchProduct(id: 1)
}
.accessibilityIdentifier("ButtonA")
The important part is setting .accessibilityIdentifier()
so we can extract metadata from the Rust process using the Accessibility API.
Backend: Rust Input + Network Logger
This is where the real work happens.
🔹 Event Hooking
We use CGEventTap
to hook into global input events like left-clicks and key presses.
let event_mask = (1 << K_CG_EVENT_LEFT_MOUSE_DOWN) | (1 << K_CG_EVENT_KEY_DOWN);
let event_tap = CGEventTapCreate(..., event_mask, event_callback, ...);
🔹 Accessibility API
On every click, we use AXUIElementCopyElementAtPosition
to resolve which UI element is under the cursor. We extract:
PID of the owning app
Accessibility identifier (if set)
Role (e.g.
AXButton
)Optional description or label
let result = ax_ui_element_copy_element_at_position(
system_wide_element, x, y, &mut element_ref);
Then:
let pid = ax_ui_element_get_pid(element_ref);
let role = ax_ui_element_copy_attribute_value(element_ref, K_AX_ROLE_ATTRIBUTE);
🔹 Network Activity via nettop
After identifying the UI element and its app PID, we shell out to:
nettop -P -J bytes_in,bytes_out -x -l 1
We parse the output for lines matching the PID (e.g. example-mac-app.47727
), and log any change in byte counts. This gives us a primitive—but real—view into per-process network activity.
Log Output Example
📡 example-mac-app.47727 ↑ 6092 B ↓ 0 B (Δ ↑ 6092 ↓ 0)
[INFO] Button Clicked: App='example-mac-app', PID=47727, ID='ButtonA', Label='Button A'
Each input event is logged with as much context as we can scrape. You can see which button triggered the traffic, and how much was sent/received.
Installation & Permissions
To run the watcher, build the Rust CLI and grant it accessibility permissions:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cd macos-watcher
cargo build --release
./target/release/macos-watcher
macOS will prompt you to allow the binary under:
System Settings → Privacy & Security → Accessibility
Once granted, you'll see logs at:
~/macos_watcher.log
You can also run it persistently in the background using the included .plist
and install_
daemon.sh
.
Dev Notes
Built using core-foundation, objc, and
simplelog
All UI inspection is done using C-level bindings to
AXUIElement*
APIsThere's no NSAccessibility or AppKit dependency in the Rust half
SwiftUI code is sandboxed and declarative. No extra work needed to simulate real-world interaction
Use Cases
Security - See if random apps are sending packets when buttons are clicked
Testing - Ensure your app only hits the network when expected
Debugging - Tie UI behavior to system-level effects
Education - Learn how accessibility and input work on macOS
Limitations
Requires GUI permission to run (can't be run headless)
nettop
output is undocumented and may changePacket contents aren't inspected—this is about attribution, not introspection
Only tracks visible, clickable elements (not gestures, background jobs, etc.)
Cross-Platform Parity
We now have both Windows and macOS versions of this tool, each using the platform's native APIs:
Feature | Windows | macOS |
Input Hooking | Win32 Low-Level Hooks | CGEventTap |
UI Element Metadata | UIAutomation / IAccessible | AXUIElement |
Network Tracking | GetExtendedTcpTable | nettop |
Language | Rust | Rust + SwiftUI |
Future Work
Add packet inspection (via
libpcap
) for HTTP tracingTag UI actions with timestamps to correlate more precisely with network
Export events in JSON or send to external observability tools
Closing Thoughts
This project taught me a lot about how macOS input and accessibility work under the hood. The fact that you can hook into system-wide mouse events, pull out UI metadata, and match it to network traffic—all with a user-space Rust binary—is kind of wild.
If you want to analyze how apps behave without reverse-engineering them from scratch, this tool gives you a solid start.
Code is available here.
Subscribe to my newsletter
Read articles from Stephen Collins directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Stephen Collins
Stephen Collins
Senior Software engineer currently working with a climate-tech startup