iOS
Record WKWebView bridge traffic in an iOS app and inspect it in the desktop app
The Dari Swift package records your WKWebView bridge traffic and sends it to the same desktop app you use for Android. The list, details, filters, masking, search, and export work the same way. There is no in-app inspector screen on iOS.
iOS connections are macOS-only: apps connect from an iOS Simulator, or from an iPhone or iPad attached to the Mac over USB. Building and installing an iOS debug build already needs a Mac, so the desktop app on Windows and Linux shows Android devices only. Connections over Wi-Fi are not offered.
Requirements
- iOS 15 or later
- The Dari desktop app on a Mac. iOS connections need macOS: Simulators run only there, and USB devices are reached through macOS's usbmuxd.
- For a physical device: a USB cable, the device unlocked once with Trust This Computer accepted, and the app installed from Xcode
- adb is optional. Without it, the desktop app shows only iOS apps.
Install
- In Xcode, choose File → Add Package Dependencies… and enter
https://github.com/easyhooon/dari. - Choose Up to Next Major Version from
3.0.0. - Add the Dari library to your app target.
With Package.swift:
.package(url: "https://github.com/easyhooon/dari", from: "3.0.0")
// target dependencies
.product(name: "Dari", package: "dari")Compatibility
- The iOS SDK and the Android library share the version line from 3.0.0. iOS needs Dari desktop 3.0.0 or later.
- The wire protocol stays at version 1, so desktop 3.x still connects to Android apps on Dari 2.x, and desktop 2.x keeps working with Android apps on 3.x.
- Android apps need no migration for 3.0.0. The handshake gains optional
platformandosVersionfields, which older peers ignore.
Start Dari
Call start() once, early in app launch, and only in builds you inspect.
import Dari
@main
struct MyApp: App {
init() {
#if DEBUG
try? Dari.shared.start()
Dari.shared.setEnvironment(["server": "dev"]) // optional labels shown in the desktop app
#endif
}
// ...
}While Dari is running, any process that can reach the device's loopback can read the recorded messages. Never call start() in an App Store build.
start() takes a DariConfiguration:
| Option | Default | Meaning |
|---|---|---|
maxEntries | 500 | Messages kept in memory; the oldest are dropped first |
maxContentLength | 500,000 | Longer payloads are truncated, as on Android |
fireAndForget | false | Record requests as finished when they are sent |
ports | 38670...38677 | Loopback ports to try; the desktop app probes the same range |
Record bridge calls
Create one interceptor per WKWebView, then call it where your bridge receives and answers messages. The methods match Android's DariInterceptor.
final class Bridge: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
private let dari = Dari.shared.interceptor(tag: "main")
weak var webView: WKWebView?
// Requests recorded afterwards carry this page URL, without its query or fragment.
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
dari.onPageUrlChanged(webView.url?.absoluteString)
}
// Web → App
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
let body = message.body as? [String: Any] ?? [:]
let handler = body["handler"] as? String ?? message.name
let requestId = body["requestId"] as? String
dari.onWebToAppRequest(handlerName: handler, requestId: requestId, requestData: body["data"] as? String)
let result = #"{"ok":true}"#
dari.onWebToAppResponse(handlerName: handler, requestId: requestId, responseData: result, isSuccess: true)
// ...send the result back to the page
}
// App → Web
func notifyPage(_ requestId: String, _ payload: String) {
dari.onAppToWebRequest(handlerName: "onResume", requestId: requestId, data: payload)
webView?.evaluateJavaScript("onResume(\(payload))") { value, error in
self.dari.onAppToWebResponse(requestId: requestId, isSuccess: error == nil, responseData: value.map { "\($0)" })
}
}
}- Requests and responses are paired by
requestId. A response completes the newest open request with the same ID, tag, and direction. - A nil
requestId, orfireAndForget: true, records a message that is complete as soon as it is sent. - Pass
displayName:when the handler name isn't readable on its own. - Calls made before
start()are ignored.
Automatic capture without these calls, like the Android Gradle plugin, is not available yet. The options are compared in this issue comment.
Connect
- Run the app in a Simulator, or on an iPhone or iPad attached over USB.
- Open the Dari desktop app on the same Mac. Simulator apps appear under the iOS Simulator device; a USB device appears under its own name. Dari connects on its own when it finds exactly one Dari app there.
Each app listens on the first free port in 38670...38677 on 127.0.0.1, so up to eight Dari apps can run on one target at the same time. When an app relaunches, the desktop app finds it again, even if it landed on a different port.
How the connection works
The app never opens a port to the network. It listens only on its own loopback address, 127.0.0.1. The desktop app reaches that address in one of two ways.
Simulator. A Simulator app runs as a process on your Mac and shares the Mac's loopback. The desktop app connects to 127.0.0.1:38670 and the next ports directly.
USB device. An iPhone's loopback is inside the iPhone, so the Mac can't reach it over the network. macOS runs usbmuxd, the daemon Finder and Xcode use to talk to devices over the cable. A program on the Mac asks usbmuxd to connect to a port on the device, and usbmuxd turns the USB link into a byte stream to that port. This is called port tunnelling. It does on iOS what adb forward does on Android:
- The desktop app asks usbmuxd (
/var/run/usbmuxd) for the attached devices, and reads each one's name from lockdownd, the service every iOS device runs on port 62078. - For each port in
38670...38677, it asks usbmuxd to connect to that port on the device. usbmuxd answers right away when nothing listens there. - On ports that accept, the desktop app sends the Dari handshake. A Dari app answers with its bundle ID, and the app is listed.
- The connection you pick keeps using the same tunnel. Unplugging the cable closes it, and the desktop app reconnects when the device comes back.
No Wi-Fi, IP address, or extra tool is involved. A device on the same Wi-Fi but not on the cable doesn't appear.
Staging builds
Android inspects a minified, non-debuggable build (staging) by opting in through manifest metadata. iOS has no code renaming like R8. Release builds are optimized (-O, whole-module) and dead-code stripped, which doesn't affect Dari: it uses no reflection or runtime class lookups.
To inspect a release-like build, add a build configuration (for example Staging). Give it its own compilation condition and start Dari under that condition:
- In the Staging configuration, add
DARIto Active Compilation Conditions (SWIFT_ACTIVE_COMPILATION_CONDITIONS). - Start Dari with
#if DEBUG || DARI.
The sample app has Debug, Staging, and Release configurations set up this way.
Swift Package Manager can't link a package to some configurations only. The Dari code (about 170 KB, arm64) stays in a Release binary even when the app never calls start(). It stays inert there: no socket opens and nothing is recorded. If that size matters, keep Dari out of the App Store target altogether, for example with a separate debug target.
Sample app
sample-ios/DariSample.xcodeproj loads a page with Web → App and App → Web scenarios: success, error, slow response, no response, fire-and-forget, and masking. Open it in Xcode 16 or later and run the DariSample scheme on a Simulator.
Troubleshooting
- The app isn't listed. The app must be in the foreground. Suspended Simulator apps don't answer. Check that
start()ran and that something is listening:lsof -nP -iTCP:38670-38677 -sTCP:LISTEN. - Two Simulators run the same app. The desktop app connects to the first one it finds. Quit one of them.
- The iPhone isn't listed. Unlock it and accept Trust This Computer, then check that Finder shows it. A device shows up only over the cable.
- The iPhone is listed but the app isn't. Bring the app to the foreground. iOS suspends apps in the background, and a suspended app can't answer.
start()throwsnoFreePort. Eight Dari apps already hold the ports. Quit some, or pass a different range inDariConfiguration(ports:). The desktop app only probes the default range.