RSMultipeerConnectivity is a Swift wrapper over Apple's MultipeerConnectivity framework for building local peer-to-peer server/client flows with typed messaging.
MultipeerConnectivity is powerful but verbose. This package is useful when you want to:
- Ship LAN peer-to-peer features faster with a higher-level API.
- Keep server/client roles explicit instead of managing raw delegates manually.
- Send and receive typed
Codablemodels with less boilerplate. - Add handshake validation and controlled peer rejection.
- Integrate easily with SwiftUI state via
@Publishedproperties and callbacks.
- Simple server/client manager API.
- Typed send/receive helpers for
Codablepayloads. DataObject<T>wrapper with IDs and timestamps.- Optional handshake validation on server invitations.
- Server-side peer rejection and kick support.
- Observable state (
@Published) for SwiftUI apps. - Built-in iOS debug UI (
DebugView). - Encrypted sessions (
MCEncryptionPreference.required).
- Swift 5.9+
- Apple platforms:
- iOS 15+
- macOS 12+
- Mac Catalyst 15+
- tvOS 15+
- visionOS 1+
In Xcode:
File->Add Packages...- Use this repository URL.
- Add product
RSMultipeerConnectivity.
Or in Package.swift:
dependencies: [
.package(url: "https://github.com/RanduSoft/RSMultipeerConnectivity.git", from: "2.0.0")
]For iOS, you must configure Bonjour service discovery and local network access.
<key>NSBonjourServices</key>
<array>
<string>_randusoft-mp._tcp</string>
</array>This value is configurable, but the format must stay:
_<serviceType>._tcp
If your Config(serviceType:) is "my-service", the Bonjour entry must be:
_my-service._tcp
In Xcode, open your target and add:
Signing & Capabilities->+ Capability->Local Network
For a one-line smoke test, present DemoView:
import RSMultipeerConnectivity
import SwiftUI
DemoView()DemoView shows a server and client in the same screen so new users can quickly verify discovery, connection, and messaging.
DebugView is internal and intentionally not part of the public API.
Config(serviceType:enableLogging:)ServerConnectivityManagerClientConnectivityManagerClientHandshakeRequestServerHandshakeResponseDataObject<Content: Codable>ConnectivityError
import RSMultipeerConnectivity
import MultipeerConnectivity
let server = ServerConnectivityManager(
displayName: "Host",
config: Config(serviceType: "my-service", enableLogging: true)
)
server.onPeerConnected = { peer in
print("Connected:", peer.displayName)
}
server.onPeerDisconnected = { peer in
print("Disconnected:", peer.displayName)
}
server.onPeerRejected = { peer, reason in
print("Rejected \(peer.displayName), reason:", reason ?? "none")
}
server.invitationValidator = { peer, handshake in
guard handshake.appVersion.hasPrefix("2.") else {
return ServerHandshakeResponse(allowed: false, reason: "Unsupported app version")
}
return ServerHandshakeResponse(allowed: true)
}
server.start()import RSMultipeerConnectivity
import MultipeerConnectivity
let client = ClientConnectivityManager(
displayName: "Player 1",
config: Config(serviceType: "my-service", enableLogging: true)
)
client.onConnectionStateChange = { isConnected in
print(isConnected ? "Connected" : "Disconnected")
}
client.onKick = { reason in
print("Kicked:", reason ?? "no reason")
}
client.start()if let serverPeer = client.availablePeers.first {
let handshake = ClientHandshakeRequest(
deviceDetails: "iPhone 16 Pro",
appVersion: "2.0.0"
)
try client.connectToServer(serverPeer, handshakeRequest: handshake)
}let data = Data("hello".utf8)
try client.send(data)struct ChatMessage: Codable {
let text: String
}
try client.send(ChatMessage(text: "Hi"))try client.sendDataObject(DataObject(content: "Ping"))client.onReceive = { payload in
print("Raw bytes:", payload.data.count, "from", payload.peerId.displayName)
}let token = client.receive(ChatMessage.self) { result in
print("Message:", result.object.text, "from", result.peerId.displayName)
}
client.cancelReceiveDataObject(id: token)let token = client.receiveDataObject(String.self) { payload in
print(payload.dataObject.content, payload.dataObject.timestamp)
}
client.cancelReceiveDataObject(id: token)- If
ServerConnectivityManager.invitationValidatoris set:- Missing handshake context is rejected.
- Invalid handshake payload is rejected.
- Valid handshake is passed to your validator.
- If no validator is set:
- Invitations are accepted.
try server.kickPeer(peerId, forReason: "Not authorized")
// or
try server.kickPeers(server.connectedClients, forReason: "Server maintenance")Client onKick is called and the client disconnects.
Use DemoView for local testing/onboarding.
displayNamemust be non-empty and <= 63 UTF-8 bytes.serviceTypemust follow MultipeerConnectivity constraints.- State and receive callbacks are dispatched to the main queue.