Build Apple-native device-to-device features without building a networking stack from scratch.
Loom is a Swift package for apps that need to find other devices, connect directly, verify identity, make trust decisions, and keep working when the local network is not the whole story.
It is designed for Apple platforms, stays product-agnostic, and gives you a clean base for the part every multi-device app eventually has to build.
Used in MirageKit.
If you are building something that talks to another device, you usually end up piecing together:
- discovery
- direct connections
- identity
- trust
- remote reachability
- diagnostics
Loom gives you those building blocks as a reusable Swift package.
That means you can focus on your app's behavior instead of spending weeks rebuilding networking plumbing.
Loom is a good fit for things like:
- Mac and iPhone companion apps
- local-first collaboration tools
- device control surfaces
- host and client apps on the same network
- pro apps that need to discover and connect to nearby machines
- products that start local but eventually need remote coordination
- Nearby peer discovery over Bonjour, including peer-to-peer support
- Direct sessions built on
Network.framework - Stable device identity and key management
- Pluggable trust policy and local trust storage
- Remote reachability support with relay presence and network probing
- Bootstrap tools for flows like Wake-on-LAN and SSH handoff
- Diagnostics and instrumentation hooks
- CloudKit-backed peer sharing
- CloudKit-backed trust decisions
- Share and participant management for multi-device apps
This part matters, especially if you are new to this space.
Loom is the transport layer, not the product layer.
Loom does not decide:
- your app's protocol
- your message schema
- your UI
- your product roles
- your CloudKit schema naming
Your app owns those decisions. Loom gives you the network foundation underneath them.
Add Loom to your Package.swift:
dependencies: [
.package(url: "https://github.com/EthanLipnik/Loom.git", branch: "main")
]Then add the product you want to your target:
.target(
name: "MyApp",
dependencies: [
.product(name: "Loom", package: "Loom"),
// Add this too if you want CloudKit-backed peer sharing or trust:
// .product(name: "LoomCloudKit", package: "Loom"),
]
)The main type to understand is LoomNode.
Think of LoomNode as the networking hub for one part of your app. It owns discovery, advertising, sessions, and the identity and trust collaborators you inject into it.
import Loom
let node = LoomNode(
configuration: LoomNetworkConfiguration(
serviceType: "_myapp._tcp",
enablePeerToPeer: true
),
identityManager: LoomIdentityManager.shared
)If you are just getting started, that is the right mental model:
- choose a Bonjour service type for your app
- decide whether peer-to-peer browsing should be enabled
- create one
LoomNodefor the runtime surface you are building
import Foundation
let identity = try LoomIdentityManager.shared.currentIdentity()
let advertisement = LoomPeerAdvertisement(
deviceID: UUID(),
identityKeyID: identity.keyID,
deviceType: .mac,
metadata: [
"myapp.role": "host",
"myapp.protocol": "1",
]
)
let port = try await node.startAdvertising(
serviceName: "My Mac",
advertisement: advertisement
) { session in
session.start(queue: .main)
}
print("Advertising on port \(port)")This makes your device discoverable and hands you a LoomSession when someone connects.
In a real app, keep deviceID stable instead of generating a new UUID() every launch. Your identity story gets much simpler if the device can be recognized over time.
let discovery = node.makeDiscovery()
discovery.onPeersChanged = { peers in
for peer in peers {
print("Found \(peer.name) at \(peer.endpoint)")
}
}
discovery.startDiscovery()At this stage, you are discovering peers and reading their advertised metadata. Your app still decides whether a peer is compatible, trusted, or worth connecting to.
import Network
let connection = NWConnection(to: peer.endpoint, using: .tcp)
let session = node.makeSession(connection: connection)
session.setStateUpdateHandler { state in
print("Session state:", state)
}
session.start(queue: .main)Once the session exists, your app takes over again. That is where your own protocol, handshake, message framing, and product logic should live.
If you only remember one thing, make it this:
LoomNodemanages discovery and connections.LoomPeerAdvertisementtells other devices what you want them to know.LoomSessionis the live connection.- Your app owns everything above that line.
That split is what keeps Loom reusable instead of turning it into someone else's app framework.
- Swift 6.2+
- macOS 14+
- iOS 17.4+
- visionOS 26+
If you want the deeper material, go to the docs:
swift build
swift test --scratch-path .build-local