Introducing swift-ros2: a native Swift client library for ROS 2, now open source

:rocket: Excited to share that swift-ros2 — the wire layer that powers the Conduit iOS app:

is now available as a standalone open-source Swift package under Apache 2.0:

Since its App Store debut in January, Conduit peaked at #4 in the Developer Tools category and has been used by over 10,000 ROS 2 developers worldwide. The feedback from that community made it clear: the real value wasn’t only the iOS app — it was the Swift-side ROS 2 wire stack underneath it. So as part of this open-source release, that stack has been extracted, hardened, extended with a full CycloneDDS transport alongside the original Zenoh one, and ported to Linux — so any Swift project, on any Apple platform or on Ubuntu, can now speak ROS 2 natively without dragging in rcl/rclcpp.

Zenoh Pub/Sub between Mac and Ubuntu using swift-ros2:

DDS Pub/Sub between Mac and Ubuntu using swift-ros2:

:bullseye: What it does

swift-ros2 is a pure-Swift ROS 2 client library that publishes and subscribes directly at the wire level — no bridge, no RCL, no Python, no C++ glue in your app.

  • Dual transport, same API. Talk to rmw_zenoh_cpp via zenoh-pico, or to rmw_cyclonedds_cpp via CycloneDDS. Swap between them with a single config change.
  • Multi-distro wire format. Humble, Jazzy, Kilted, Rolling — all four are covered by the built-in wire codecs.
  • Pure-Swift XCDR v1 codec. Serialization is verified against golden-byte fixtures extracted from real ROS 2 traffic.
  • 20 built-in message types across sensor_msgs, geometry_msgs, std_msgs, audio_common_msgs, and tf2_msgs. Custom messages are a single ROS2Message conformance away.
  • Swift-native API. async/await, AsyncStream subscriptions, Sendable conformance, structured concurrency — the kind of API Swift developers actually want.

:laptop: Platforms

Platform Integration
iOS / iPadOS 16+ Pre-built xcframework via SPM
macOS 13+ Pre-built xcframework via SPM
Mac Catalyst 16+ Pre-built xcframework via SPM
visionOS 1.0+ Pre-built xcframework via SPM
Linux (Ubuntu 22.04 / 24.04, x86_64 + aarch64) swift build from source

CI exercises the full matrix — Swift 6.0.2 on Ubuntu with Humble (22.04), Jazzy (24.04), and Rolling (24.04), on both x86_64 and aarch64 — plus Xcode 16.2 on Apple Silicon:

:technologist: Publish in five lines

import SwiftROS2

let context = try await ROS2Context(
    transport: .zenoh(locator: "tcp/192.168.1.100:7447"),  // or: .ddsMulticast(domainId: 0)
    distro: .jazzy
)
let node = try await context.createNode(name: "sensor_node", namespace: "/macos")
let pub = try await node.createPublisher(Imu.self, topic: "imu")
try pub.publish(Imu(header: .now(frameId: "imu_link"), linearAcceleration: .init(x: 0, y: 0, z: 9.81)))

Switching to DDS is a one-line change to the transport: argument. Subscribers use the same Node API and expose an AsyncStream of typed messages.

Runnable talker / listener demos modeled on demo_nodes_cpp ship in Sources/Examples/ and interop directly with ros2 topic echo.

:white_check_mark: Today

  • Publishers and subscribers for both Zenoh and DDS transports
  • XCDR v1 serialize + deserialize
  • Jazzy / Humble / Kilted / Rolling wire codecs
  • 20 built-in message types
  • Custom message support via ROS2Message protocol

:motorway: Roadmap

  • Services (request/reply) and Actions (goal/feedback/result)
  • swift-ros2-gen — code generator for .msg / .srv / .action files, so any ROS 2 package’s interfaces can be consumed without hand-porting
  • Expanded message catalognav_msgs, visualization_msgs, and more
  • Continued hardening of the DDS subscriber path and wider distro coverage

:raising_hands: Who this is for

  • iOS / macOS / visionOS developers who want to ship ROS 2 apps without a sidecar bridge
  • Robotics teams looking for a lightweight Linux Swift client (agents, microservices, tooling)
  • Anyone who’s wanted a Swift-native way to speak to rmw_zenoh_cpp or rmw_cyclonedds_cpp

Issues, PRs, and wire-format war stories are all welcome!

7 Likes

A few notable updates have landed since the original post:

  • Windows 10/11 (x86_64), Zenoh only. zenoh-pico builds from source through SwiftPM with the Winsock + Iphlpapi backend — no setup.bash, no CMake bootstrap. Requires Swift 6.3.1. Targeting 0.7.0; already on main.
  • Android API 28+ (arm64-v8a, x86_64), Zenoh only. zenoh-pico source-builds with the Bionic unix backend, cross-compiled from macOS or Linux against the swift.org Android Swift SDK. Shipped in 0.5.0.
  • CI now exercises every supported OS on every push. The ci.yml matrix runs build-macos, build-linux (3 distros × 2 arches), build-windows, and build-android (2 ABIs), plus an Android x86_64 emulator job that drives the test suite end-to-end on Bionic — four CI badges in the README, one per OS family.
  • Consumer-OS coverage is now quantified. A new README.md table sourced from Statcounter (March 2026) shows swift-ros2 reaches ≈99.7% of the mobile market (iOS + Android), ≈78.7% of the desktop market (Windows + macOS + Linux), and ≈90.7% across all device classes — effectively every consumer endpoint a ROS 2 graph might want to talk to.

swift-ros2 1.1 — Services, Actions, Parameters, IDL codegen, plus Windows & Android

Hi all — a follow-up to the original announcement from earlier this year. swift-ros2 has shipped 0.5 through 1.1 since then, and the surface area has grown enough that it felt worth a recap rather than burying it in the release notes.

Quick reminder of what swift-ros2 is: a pure-Swift ROS 2 client library that talks to rmw_zenoh_cpp and rmw_cyclonedds_cpp at the wire level — no rcl, no rclcpp, no Python/colcon. Repo: github.com/youtalk/swift-ros2. Apache 2.0.

What’s new

Services — full Server + Client API

rclcpp / rclpy-shaped ROS2Service<S> / ROS2Client<S> with async/await, working across Humble / Jazzy / Kilted / Rolling on both Zenoh and DDS.

// Server
let svc = try await node.createService(AddTwoInts.self, name: "/add_two_ints") { req in
    AddTwoInts.Response(sum: req.a + req.b)
}

// Client
let cli = try await node.createClient(AddTwoInts.self, name: "/add_two_ints")
try await cli.waitForService(timeout: .seconds(2))
let resp = try await cli.call(.init(a: 2, b: 3), timeout: .seconds(5))

Actions — full Server + Client API

Typed ROS2ActionServer<H> / ROS2ActionClient<A> with goal handles, feedback as AsyncStream, status, and cancellation. Built-in example_interfaces/action/Fibonacci. No callback shims.

let client = try await node.createActionClient(FibonacciAction.self, name: "/fibonacci")
try await client.waitForActionServer(timeout: .seconds(5))

let handle = try await client.sendGoal(.init(order: 10))

Task {
    for await fb in handle.feedback {
        print("partial: \(fb.sequence)")
    }
}

switch try await handle.result() {
case .succeeded(let r): print("got \(r.sequence)")
case .canceled:        print("canceled")
case .aborted(let r):  print("aborted: \(r ?? "nil")")
}

Parameters — node-side storage + remote client

Every ROS2Node declares/gets/sets parameters, exposes the six standard parameter services, emits /parameter_events, and supports on/pre/post-set callbacks. ros2 param list / get / set from the CLI round-trips with a swift-ros2 node.

// Declare with a default and a descriptor (range / read-only / etc.)
try await node.declareParameter("max_velocity", default: 1.5)

// Read it back from anywhere in the app
let v: Double = await node.getParameterOrDefault("max_velocity", default: 0.0)

// Reject changes to "frozen" from any source — CLI, remote client, or local set
node.addOnSetParameterCallback { params in
    .init(successful: params.allSatisfy { $0.name != "frozen" })
}

swift-ros2-gen — IDL → Swift code generation

A CLI plus SwiftPM build plugin that emit ROS2Message / ROS2ServiceType / ROS2Action conformances directly from .msg / .srv / .action. Multi-distro merging: when the same package’s IDL differs across distros (e.g. sensor_msgs/Range.variance arriving in Jazzy), one generated Swift file branches on isLegacySchema. Hash-oracle CI cross-checks every generated type hash against a live ROS 2 environment, so wire drift is a build failure, not a runtime mystery.

1.0.0 — API freeze

1.0 inaugurated the SemVer 1.x line. The public surface (ROS2Context, ROS2Node, ROS2Publisher, ROS2Subscription, ROS2Service, ROS2Client, ROS2ActionServer, ROS2ActionClient, QoSProfile, TransportConfig, the concrete ZenohClient / DDSClient, every ROS2Message/ROS2ServiceType/ROS2Action) won’t break on any 1.x minor or patch. Internal plumbing was pulled out of the public namespace at the cut — there’s a MIGRATION.md covering every rename.