A real Xcode project. A real distributed actor. Apple's own Distributed
runtime executing the call. The caller is a web page.
Safari / Chrome iPhone Simulator
───────────────── ────────────────────────────
miniswift.run/studio ──HTTP──▶ DistDemo.app
try await phone.hello() executeDistributedTarget(...)
(compiled to WebAssembly (real Swift, real Apple runtime)
in the browser) ◀──bytes──
- Open
DistDemo.xcodeproj, pick any iPhone simulator, press ▶. The app shows Listening · 127.0.0.1:8891. - Open https://miniswift.run/studio/?demo=simulator and press ▶ Run.
You should see the phone answer, and each call appear live on the phone screen.
Hello world — from the iPhone Simulator, running real Swift
iPhone 17 Pro · iOS 27.0
42
An HTTPS page calling a local address is gated by Chrome's Local Network Access permission. Allow it when Chrome asks, or use Safari or Firefox, which do not apply this restriction. This is a browser policy — CORS headers do not change it. (The server already sends permissive CORS and answers the OPTIONS preflight, which application/octet-stream requires.)
Apple ships no network transport for distributed actors. The only concrete system in the SDK is LocalTestingDistributedActorSystem, and it is in-process. But DistributedActorSystem is a public protocol and
Distributed.swiftmodule is available for iphonesimulator, so you can write your own. ActorSystem.swift is one.
The call itself is not interpreted by this code. It is handed to executeDistributedTarget(on:target:invocationDecoder:handler:) — Apple's own machinery — which is why this counts as real interop rather than a lookalike.
deviceName() exists to settle the obvious objection: the browser asks the phone what it is, and the phone answers. Nothing on the JavaScript side can invent that.
| file | |
|---|---|
DistDemo/Wire.swift |
the byte format, nothing else |
DistDemo/ActorSystem.swift |
DistributedActorSystem conformance + dispatch |
DistDemo/Greeter.swift |
the actor whose bodies actually run |
DistDemo/Server.swift |
NWListener HTTP server, loopback only |
DistDemo/MangledTargets.swift |
generated — see below |
Tools/sync-targets.sh |
regenerates that table from the built binary |
The source has no comments, so the traps that cost real time live here.
Hold the listener. Server keeps its NWListener in a property. When it was a local let inside start(), the listener was released on return: the kernel socket stayed open so lsof showed LISTEN and TCP connected — but newConnectionHandler was dead, so requests were never read and the client hung with no reply. Two earlier hypotheses (IPv6, iOS Local Network permission) were both wrong.
Bind loopback only. NWListener(using: .tcp, on: port) listens on every interface, which on iOS falls under Local Network privacy: the listener reports ready, the kernel accepts TCP, and newConnectionHandler never fires. requiredLocalEndpoint pins it to 127.0.0.1, outside that scope.
Keep the actor and the server alive. Both are globals. As locals in init they are released when it returns, which reproduces the "connects, no reply" symptom above.
The decoder is inout. executeDistributedTarget consumes it.
Text("…:\(port)") localizes the number. Text takes a LocalizedStringKey, so the port rendered as 8.891. Build the string with String(port).
pending must be @Published. It backs the "N waiting" label through a computed property. As a plain var the label froze: the browser collected the message and the count never moved, so it looked undelivered when it was not.
The wire is not symmetric. Bool is 1 byte, Int is 8. Reads are done byte by byte because the wire is packed and load(as:) needs alignment.
MiniSwift names a target Greeter.hello; Swift names the same method $s8DistDemo7GreeterC5helloyS2SYaKFTE. The mangled form depends on the module name and the signature, so it cannot be written by hand — it is read out of the compiled binary.
Renaming a method still builds clean. What breaks is runtime: the server no longer finds the target and returns 404, with no clue anywhere. After any change to Greeter, build once and run:
Tools/sync-targets.sh /path/to/Build/Products/Debug-iphonesimulator/DistDemo.appthen build again.
A web page cannot open a listening socket, so the direction is fixed: the browser calls, the phone answers. For browser-to-browser there is a separate transport that uses no network at all — https://miniswift.run/studio/?demo=distributed runs a distributed actor across two tabs.