Native Swift client library for ROS 2. Publish and subscribe to ROS 2 topics over Zenoh or DDS on every consumer device OS that runs Swift — no rcl cross-compile, no bridge. A single .package(url:) on Apple, apt install ros-<distro>-cyclonedds on Linux, a vanilla swift build on Windows / Android. The API is Swift-native (async/await, AsyncStream, Sendable) and round-trip compatible with rmw_zenoh_cpp / rmw_cyclonedds_cpp.
Shipping on the SemVer-stable 1.x line (latest tag in the release badge). Extracted from Conduit, powered by ROS — used cumulatively by 10,000+ ROS developers and a former #4 in the App Store's Developer Tools category.
Android: the umbrella is excluded on Android — use
SwiftROS2Zenoh.ZenohClientdirectly until DDS (and the umbrella) land there.
import SwiftROS2
// Publish an IMU message over Zenoh.
let context = try await ROS2Context(
transport: .zenoh(locator: "tcp/192.168.1.100:7447"), distro: .jazzy)
let node = try await context.createNode(name: "sensor_node", namespace: "/ios")
let pub = try await node.createPublisher(Imu.self, topic: "imu")
try pub.publish(Imu(
header: Header.now(frameId: "imu_link"),
linearAcceleration: Vector3(x: 0, y: 0, z: 9.81)))
// Same over DDS — identical Node / Publisher / Subscription API from here on.
let ddsContext = try await ROS2Context(transport: .ddsMulticast(domainId: 0))
// Subscribe.
let sub = try await node.createSubscription(Imu.self, topic: "imu")
for await msg in sub.messages { print("accel: \(msg.linearAcceleration)") }ROS2Service<S> / ROS2Client<S> round-trip a typed request / response over either transport (Zenoh queryables or DDS rq/rr topics). Failures surface as ServiceError (.timeout, .handlerFailed, .serviceUnavailable, .taskCancelled, …).
let svc = try await node.createService(TriggerSrv.self, name: "/trigger") { _ in
TriggerSrv.Response(success: true, message: "ok")
}
let cli = try await node.createClient(TriggerSrv.self, name: "/trigger")
try await cli.waitForService(timeout: .seconds(5))
let resp = try await cli.call(.init(), timeout: .seconds(5))Declare typed parameters (node.declareParameter + on-set veto callbacks; interoperates with ros2 param list/set and /parameter_events) and typed actions (node.createActionServer / createActionClient with goal handles and feedback AsyncStream). End-to-end talker / listener demos modeled on demo_nodes_cpp — swift run talker zenoh, swift run listener dds, swift run parameter-demo zenoh — live under Sources/Examples/README.md.
Two backends sit behind one backend-agnostic umbrella API. .zenoh(locator:) / .dds(...) resolve automatically:
- Native RCL backend (recommended, default where available) — the real upstream stack (
rcl+rmw_zenoh_cpp/rmw_cyclonedds_cpp), so type hashes, QoS semantics, the node graph, and introspection match upstream by construction. As of 1.3.0 it is available on Apple (prebuiltCRos2Jazzy/CRos2JazzyZenohxcframeworks, one rmw baked per build variant, on by default) and Linux (system ROS 2 install viaROS2_RCL_PREFIX, rmw chosen at runtime from the transport type). - Pure-Swift wire path (
zenoh-pico/ CycloneDDS, norcl) — the original all-platforms backend. It remains the automatic fallback where RCL isn't available yet (Android; visionOS zenoh; Windows) and the golden-byte oracle for the CDR / wire codecs.
The wire path is deprecated as of 1.3.0 and will be removed in 2.0.0. Only direct construction of the wire clients is deprecated:
import SwiftROS2Zenoh
let client = ZenohClient() // ⚠️ deprecated, removed in 2.0.0
import SwiftROS2DDS
let dds = DDSClient() // ⚠️ deprecated, removed in 2.0.0The umbrella API is unchanged and not deprecated — most consumers need no change:
let ctx = try await ROS2Context(transport: .zenoh(locator: "tcp/192.168.1.85:7447"))If you only build ZenohClient / DDSClient to hand to ROS2Context, drop the explicit construction. If you use them standalone (raw key-expression puts, wire-level subscribers), plan the move to the umbrella API before 2.0.0 — the wire runtime path is removed there, while the CDR / wire codecs survive as golden-byte fixtures. Full recipes in MIGRATION.md.
Per-variant nuance. In the Apple zenoh-rmw RCL variant (
SWIFT_ROS2_RCL_RMW=zenoh) the zenoh wire family is physically absent (zenoh-pico and the bundled zenoh-c export the same C symbols and cannot co-link) —ZenohClientdoesn't exist there at all. On Linux RCL builds both backends stay linked (rmw is a dlopen'd plugin); RCL is preferred at runtime. Windows RCL is deferred — no official Jazzy Windows binary shipsrmw_zenoh_cppand swift-ros2's RCL layer is Jazzy-pinned; re-gates on an official Jazzy binary or Kilted support. Android RCL (full-rclNDK cross-build) remains unsolved.
1.0.0 inaugurated the SemVer 1.x line: no minor or patch release breaks the public API — breaking changes require a 2.0 bump. The frozen surface covers ROS2Context, ROS2Node, ROS2Publisher, ROS2Subscription, ROS2Service, ROS2Client, ROS2ActionServer, ROS2ActionClient, QoSProfile, TransportConfig, the concrete ZenohClient / DDSClient, and every ROS2Message / ROS2ServiceType / ROS2Action type. See MIGRATION.md for the internal-plumbing demotions made at the 1.0 cut and the 2.0 wire-removal plan.
- Dual transport, two backends, one API.
.zenoh(locator:)/.dds(...)resolve to the RCL backend where it exists and the wire path elsewhere. Switch transports with a singleTransportConfigchange. - No mandatory
rcltoolchain. Apple downloads prebuilt xcframeworks (including the RCL variants); Linux uses a system ROS 2 install for RCL; the wire path needs no ROS 2 install at all (Windows resolves CycloneDDS viavcpkg; Android is Zenoh-only). - Swift-native API.
async/awaiteverywhere,AsyncStreamsubscriptions,Sendable, structured concurrency, no opaque pointer juggling above the FFI seam. - Multi-distro. Humble, Jazzy, Kilted, Rolling — select via
ROS2Distro(Zenoh defaults to Jazzy). Schema differences (e.g.sensor_msgs/Range.variance, added after Humble) are gated automatically viaisLegacySchema. - 23 built-in message types across
sensor_msgs,geometry_msgs,std_msgs,audio_common_msgs,tf2_msgs, on a pure-Swift XCDR v1 encoder + decoder. - Services & Actions —
rclcpp/rclpy-shapedROS2Service/ROS2Clientand typedROS2ActionServer<H>/ROS2ActionClient<A>(goal handles, feedbackAsyncStream, cancellation) over both transports, all distros. - Parameters — every
ROS2Nodedeclares typed parameters with descriptors / ranges, auto-registers the six standardrcl_interfacesservices, and publishes/parameter_events; interoperates withros2 param. - Code generation from IDL.
swift-ros2-genCLI + SwiftPM build plugin emitROS2Message/ROS2ServiceType/ROS2Actionconformances from.msg/.srv/.action, with multi-distro merging and a hash-oracle CI that catches drift against live ROS 2.
| Platform | Minimum target | Integration path | Transports |
|---|---|---|---|
| iOS / iPadOS | 16.0 | binaryTarget xcframework |
Zenoh + DDS |
| macOS | 13.0 | binaryTarget xcframework |
Zenoh + DDS |
| Mac Catalyst | 16.0 | binaryTarget xcframework |
Zenoh + DDS |
| visionOS | 1.0 | binaryTarget xcframework |
Zenoh + DDS |
| Linux | Ubuntu 22.04 / 24.04 (x86_64, aarch64) | zenoh-pico source build + pkg-config for DDS |
Zenoh + DDS |
| Windows | Windows 10 / 11 (x86_64) | zenoh-pico source build (Winsock) + vcpkg for DDS |
Zenoh + DDS |
| Android | API 28+ (arm64-v8a, x86_64) | zenoh-pico source build (Bionic, unix backend) |
Zenoh only |
Swift 5.9+ on Apple; the CI matrix is unified on Swift 6.3.1 across macOS (Xcode 26.4.1), Linux, Windows, and Android. By worldwide market share (Statcounter, March 2026) swift-ros2 covers ≈99.7% of the mobile market and ≈90.7% of identifiable device share — nearly every consumer device you might attach to a ROS 2 graph. Non-host Apple slices are built end-to-end by release-xcframework.yml at tag time; per-push iOS / visionOS / Mac Catalyst runtime validation comes from Conduit rather than CI. Architecture: docs/ARCHITECTURE.md.
// Package.swift
dependencies: [
.package(url: "https://github.com/youtalk/swift-ros2.git", from: "1.0.0"),
],
targets: [
.target(name: "YourApp", dependencies: [
.product(name: "SwiftROS2", package: "swift-ros2"),
]),
]swift build downloads the pinned xcframeworks in seconds — no CMake, no local bootstrap. import SwiftROS2 exposes ROS2Context / ROS2Node / ROS2Publisher / ROS2Subscription and transitively links SwiftROS2Zenoh + SwiftROS2DDS; add those to your target dependencies only to name ZenohClient / DDSClient directly. (The URL pin lags one PR behind each tag — pinning from: "X.Y.Z" resolves to the X.Y.Z commit; tracking main always picks up the latest pinned binaries.)
sudo apt install ros-jazzy-cyclonedds # or ros-humble / ros-rolling
git clone --recursive https://github.com/youtalk/swift-ros2.git && cd swift-ros2
bash Scripts/build-linux-deps.sh # verifies pkg-config finds CycloneDDS
# Re-export in the current shell — build-linux-deps.sh sets these only in its subprocess.
source /opt/ros/jazzy/setup.bash
export PKG_CONFIG_PATH=/opt/ros/jazzy/lib/$(uname -m)-linux-gnu/pkgconfig:$PKG_CONFIG_PATH
swift build
swift test # 69 pass, 2 LINUX_IP-gated skipsBoth transports. Zenoh builds from source with no extra setup; DDS resolves CycloneDDS through vcpkg. Requires Swift 6.3.1. Without CYCLONEDDS_DIR, the manifest stays Zenoh-only.
vcpkg install cyclonedds:x64-windows
$env:CYCLONEDDS_DIR = "$env:VCPKG_ROOT\installed\x64-windows"
$env:Path = "$env:CYCLONEDDS_DIR\bin;$env:Path" # so ddsc.dll resolves at runtimeThen add the same .package(url:) dependency as Apple. swift build threads -I<dir>/include + -L<dir>/lib into CDDSBridge from CYCLONEDDS_DIR — no setup.bash / PKG_CONFIG_PATH step.
Install the Swift 6.3.1 Android SDK from swift.org/install/android, install Android NDK ≥ r27, then run the bundled setup-android-sdk.sh once to symlink the NDK sysroot into place (see swift.org for the exact incantation).
swift sdk install <android-sdk-url> --checksum <sha>
SWIFT_ROS2_TARGET_OS=android swift build --swift-sdk aarch64-unknown-linux-android28
# or x86_64-unknown-linux-android28 for emulator targetsSWIFT_ROS2_TARGET_OS=android is required for any cross-compile — SwiftPM evaluates manifest-scope #if os(...) against the host, so without it a Linux host pulls in the un-buildable DDS path and a macOS host never source-builds zenoh-pico. The value is allow-list-validated ({android, apple, linux, windows}); typos fail the manifest compile. The SwiftROS2 umbrella isn't built on Android — import SwiftROS2Zenoh directly.
import SwiftROS2 // public API — re-exports CDR / Messages / Transport / Wire
├── SwiftROS2CDR — XCDR v1 encoder + decoder (pure Swift, no deps)
├── SwiftROS2Wire — Zenoh / DDS wire codecs, ROS2Distro, TypeNameConverter
├── SwiftROS2Messages — ROS2Message protocols + 23 built-in types
└── SwiftROS2Transport — TransportSession / TransportConfig / EntityManager / GIDManager
// Depended on by SwiftROS2 (so the high-level API works after `import SwiftROS2`),
// but NOT @_exported — import explicitly to reach ZenohClient / DDSClient.
import SwiftROS2Zenoh — ZenohClient (zenoh-pico FFI through CZenohBridge)
import SwiftROS2DDS — DDSClient (CycloneDDS FFI through CDDSBridge)
CZenohPico / CCycloneDDS are link-only C targets — consumed at the C level by CZenohBridge / CDDSBridge and never imported from Swift (a CI lint guards against it). Reach the C bridges via import SwiftROS2Zenoh / import SwiftROS2DDS.
sensor_msgs(13) —BatteryState,CameraInfo,CompressedImage,FluidPressure,Illuminance,Image,Imu,Joy,MagneticField,NavSatFix,PointCloud2,Range,Temperaturegeometry_msgs(3 publishable + utility types) —PoseStamped,TransformStamped,TwistStamped; sub-typesVector3/Quaternion/Point/Pose/Twist/Transformstd_msgs(5) —BoolMsg,EmptyMsg,Float64Msg,Int32Msg,StringMsg; plus the universalHeaderaudio_common_msgs(1) —AudioData·tf2_msgs(1) —TFMessage
swift-ros2-gen reads .msg / .srv / .action IDL and emits Swift ROS2Message / ROS2ServiceType / ROS2Action conformances — the recommended way to produce these since 0.9.0. Pass one --input <pkg>=<path>@<distro> per (package, distro) pair (every transitively-referenced package must appear so nested-type resolution succeeds); the generator merges multi-distro inputs into one Swift source that branches on isLegacySchema.
swift run swift-ros2-gen \
--input "builtin_interfaces=vendor/rcl_interfaces-jazzy/builtin_interfaces@jazzy" \
--input "std_msgs=vendor/common_interfaces-jazzy/std_msgs@jazzy" \
--input "geometry_msgs=vendor/common_interfaces-jazzy/geometry_msgs@jazzy" \
--input "sensor_msgs=vendor/common_interfaces-jazzy/sensor_msgs@jazzy" \
--output Sources/MyMessages/GeneratedFor always-in-sync bindings, add the SwiftROS2GenPlugin build plugin to a target (single-package, single-distro, .msg-only; name the target snake_case — it becomes the ROS package segment). A worked setup is at Sources/Examples/PluginSmoke/. The verify-hash-oracle CI job diffs each generated RIHS01_* hash against an osrf/ros:<distro>-desktop image; reproduce locally with --verify-hashes osrf/ros:jazzy-desktop.
Custom Conduit-style types are hand-written under Sources/Messages/ with the same ROS2Message conformance (typeInfo + encode(to:) + init(from:)).
Tags follow Apple-ecosystem bare semver (no v prefix): 0.2.0, 1.0.0-rc.1, … The release workflow fires on any tag matching [0-9]*.[0-9]*.[0-9]* (optionally -qualifier), builds both xcframeworks for all six Apple slices, and attaches them + .checksum files to the GitHub release. The pin URL + checksums are bumped in a follow-up PR (GitHub re-zips on upload, so server-side checksums must be re-computed with swift package compute-checksum).
Each release has a GitHub release with full notes; headlines below.
| Tag | Date | Headline |
|---|---|---|
| 1.3.0 | 2026-07-13 | RCL everywhere; wire path deprecated. Native rcl + rmw_zenoh_cpp / rmw_cyclonedds_cpp backend on Apple (default-on, per-variant xcframeworks) and Linux (system ROS 2). Direct ZenohClient() / DDSClient() construction is deprecated — removed in 2.0.0. Purely additive otherwise (#151–#174). |
| 1.2.0 | 2026-06-06 | Source-timestamp publish overload — additive publish(_:timestamp:sequenceNumber:) (#117). |
| 1.1.0 | 2026-05-06 | Parameter API — typed declares, the six rcl_interfaces services, /parameter_events, ROS2ParameterClient (#102–#107). |
| 1.0.0 | 2026-05-05 | API stability promise (1.x SemVer freeze) — plumbing types pulled out of the public surface; end-user types unchanged. |
| 0.9.0 | 2026-05-04 | swift-ros2-gen — IDL → Swift code generator (CLI + SwiftPM plugin) + verify-hash-oracle CI. |
| 0.8.0 | 2026-05-03 | Actions + DDS on Windows — typed ROS2ActionServer / ROS2ActionClient; CycloneDDS on Windows via vcpkg. |
| 0.7.0 | 2026-05-01 | Services — typed ROS2Service / ROS2Client over Zenoh and DDS. |
| 0.6.0 | 2026-04-24 | Android — arm64-v8a + x86_64 via the Swift Android SDK (Zenoh only). |
| 0.5.0 | 2026-04-24 | Windows x86_64 — three-arm Package.swift split, zenoh-pico source build (Zenoh only). |
| 0.4.0 | 2026-04-20 | DDS subscriber — CDDSBridge fragchain reader; swift run listener dds. |
| 0.3.0 | 2026-04-19 | API rename (breaking) — drop Default prefix from ZenohClient / DDSClient; Linux arm64 + Rolling + Humble CI. |
| 0.2.0 | 2026-04-18 | Initial public release — Publisher + Subscriber, XCDR v1 codec, Jazzy + Humble wire, dual transport. |
Concrete deliverables, not aspirational vapor.
Near-term (additive, 1.x): expanded message catalog (nav_msgs, visualization_msgs, diagnostic_msgs) via swift-ros2-gen; TF / TF2 runtime layer (TransformBroadcaster, Buffer + TransformListener, mirroring tf2_ros); logging / /rosout (Logger on ROS2Node, bridging os.Logger).
Medium-term: DDS on Android (blocked on ddsrt's CMake-time header generation under the NDK; likely a prebuilt .artifactbundle); XCDR2 wire format; LifecycleNode (nine-state machine + lifecycle_msgs); composition / intra-process short-circuit; discovery control (ROS_AUTOMATIC_DISCOVERY_RANGE, ROS_STATIC_PEERS).
Stretch: Linux static .artifactbundle; watchOS / tvOS slices; OpenXR-on-Android for Quest / Pico; rosbag2 (mcap) read/write; DDS Security; action-server fan-out; a minimal xacro / URDF parser.
PRs welcome. The wire fixtures in Tests/SwiftROS2WireTests/ and golden CDR tests in Tests/SwiftROS2CDRTests/ are the canonical guardrails — keep them green. Tests/SwiftROS2IntegrationTests/ boots a real ROS 2 subscriber; set LINUX_IP=<host> to exercise it. Lint with swift format lint --strict --configuration .swift-format Package.swift Sources Tests before pushing — CI fails lint before any build matrix runs.
Apache License 2.0. See LICENSE.