Skip to content
Skip
3k

Socket.IO

Real-time bidirectional communication with Socket.IO for Skip apps on both iOS and Android.

SkipSocketIO wraps the native Socket.IO client libraries for each platform:

Key features inherited from Socket.IO:

  • Automatic reconnection with exponential backoff
  • Packet buffering and automatic acknowledgments
  • Event-driven communication with emit and on
  • Multiplexing through namespaces
  • Transport fallback (WebSocket with HTTP long-polling fallback)

Add the dependency to your Package.swift file:

let package = Package(
name: "my-package",
products: [
.library(name: "MyProduct", targets: ["MyTarget"]),
],
dependencies: [
.package(url: "https://source.skip.dev/skip-socketio.git", "0.0.0"..<"2.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "SkipSocketIO", package: "skip-socketio")
])
]
)

Create a client and connect:

import SkipSocketIO
let socket = SkipSocketIOClient(socketURL: URL(string: "https://example.org")!, options: [
.secure(true),
.reconnects(true),
.reconnectAttempts(5),
])
socket.on(SocketIOEvent.connect) { _ in
print("Connected! Socket ID: \(socket.socketId ?? "unknown")")
}
socket.on(SocketIOEvent.disconnect) { _ in
print("Disconnected")
}
socket.on(SocketIOEvent.connectError) { data in
print("Connection error: \(data)")
}
socket.connect()

Use on for persistent listeners, or once for one-time handlers:

// Called every time the event fires
socket.on("chat message") { data in
if let message = data.first as? String {
print("Received: \(message)")
}
}
// Called once, then automatically removed
socket.once("welcome") { data in
print("Server welcome: \(data)")
}
// Remove handlers for a specific event
socket.off("chat message")
// Remove all handlers
socket.removeAllHandlers()

Listen for every incoming event:

socket.onAny { eventName, data in
print("Event '\(eventName)' received with data: \(data)")
}

Send events with data to the server:

// Simple string
socket.emit("chat message", ["Hello, world!"])
// Multiple items of different types
socket.emit("update", ["status", 42, true])
// With a send completion callback
socket.emit("save", ["data to save"]) {
print("Event sent")
}

Emit an event and receive an acknowledgement from the server:

socket.emitWithAck("get-users", ["room-1"]) { ackData in
print("Server responded with: \(ackData)")
}

Check the current connection state:

if socket.isConnected {
print("Socket ID: \(socket.socketId ?? "")")
}
switch socket.status {
case .connected: print("Connected")
case .connecting: print("Connecting...")
case .disconnected: print("Disconnected")
case .notConnected: print("Never connected")
}
socket.connect(timeoutAfter: 5.0) {
print("Connection timed out after 5 seconds")
}

Use SkipSocketIOManager to connect to multiple namespaces on the same server:

let manager = SkipSocketIOManager(socketURL: URL(string: "https://example.org")!, options: [
.reconnects(true),
.forceWebsockets(true),
])
let defaultSocket = manager.defaultSocket()
let chatSocket = manager.socket(forNamespace: "/chat")
let adminSocket = manager.socket(forNamespace: "/admin")
chatSocket.on("message") { data in
print("Chat message: \(data)")
}
adminSocket.on("notification") { data in
print("Admin notification: \(data)")
}
defaultSocket.connect()
chatSocket.connect()
adminSocket.connect()
let socket = SkipSocketIOClient(socketURL: URL(string: "https://example.org")!, options: [
// Transport
.forceWebsockets(true), // Use only WebSockets (no long-polling fallback)
.forcePolling(true), // Use only HTTP long-polling
.compress, // Enable WebSocket compression
.secure(true), // Use secure transports (wss://)
.path("/custom-path/"), // Custom Socket.IO server path
// Reconnection
.reconnects(true), // Enable auto-reconnection
.reconnectAttempts(10), // Max reconnection attempts (-1 for infinite)
.reconnectWait(1), // Min seconds between reconnection attempts
.reconnectWaitMax(30), // Max seconds between reconnection attempts
.randomizationFactor(0.5), // Jitter factor for reconnection delay
// Headers and parameters
.extraHeaders(["Authorization": "Bearer token123"]),
.connectParams(["userId": "abc"]),
.auth(["token": "secret"]),
// Other
.forceNew(true), // Always create a new engine
.log(true), // Enable debug logging
.selfSigned(true), // Allow self-signed certificates (dev only)
.enableSOCKSProxy(false), // SOCKS proxy support
])
import SwiftUI
import SkipSocketIO
struct ChatView: View {
@State var messages: [String] = []
@State var inputText: String = ""
@State var socket = SkipSocketIOClient(
socketURL: URL(string: "https://chat.example.org")!,
options: [.reconnects(true)]
)
var body: some View {
VStack {
List(messages, id: \.self) { message in
Text(message)
}
HStack {
TextField("Message", text: $inputText)
.textFieldStyle(.roundedBorder)
Button("Send") {
socket.emit("chat message", [inputText])
inputText = ""
}
}
.padding()
}
.task {
socket.on("chat message") { data in
if let msg = data.first as? String {
messages.append(msg)
}
}
socket.connect()
}
}
}

The primary interface for Socket.IO communication.

Method / PropertyDescription
init(socketURL:options:)Create a client for the given server URL
connect()Connect to the server
connect(timeoutAfter:handler:)Connect with a timeout callback
disconnect()Disconnect from the server
isConnected: BoolWhether the client is currently connected
socketId: String?The server-assigned session ID
status: SocketIOStatusCurrent connection status
on(_:callback:)Register a persistent event handler
once(_:callback:)Register a one-time event handler
off(_:)Remove all handlers for an event
removeAllHandlers()Remove all event handlers
onAny(_:)Register a catch-all handler for all incoming events
emit(_:_:completion:)Emit an event with data
emitWithAck(_:_:ackCallback:)Emit an event and receive a server acknowledgement

Manages connections and namespaces for a server.

MethodDescription
init(socketURL:options:)Create a manager for the given server URL
defaultSocket()Returns a client for the default namespace (/)
socket(forNamespace:)Returns a client for the given namespace
CaseDescription
.notConnectedHas never been connected
.connectingConnection in progress
.connectedCurrently connected
.disconnectedWas connected, now disconnected

Standard event name constants.

ConstantValueDescription
SocketIOEvent.connect"connect"Fired on successful connection
SocketIOEvent.disconnect"disconnect"Fired on disconnection
SocketIOEvent.connectError"connect_error"Fired on connection error
OptionDescriptionAndroid Support
.compressWebSocket compressionIgnored
.connectParams([String: Any])GET parameters for the connect URLIgnored
.extraHeaders([String: String])Extra HTTP headersSupported
.forceNew(Bool)Always create a new engineSupported
.forcePolling(Bool)HTTP long-polling onlySupported
.forceWebsockets(Bool)WebSockets onlySupported
.enableSOCKSProxy(Bool)SOCKS proxyIgnored
.log(Bool)Debug loggingIgnored
.path(String)Custom Socket.IO pathSupported
.reconnects(Bool)Auto-reconnectionSupported
.reconnectAttempts(Int)Max reconnection attemptsSupported
.reconnectWait(Int)Min seconds before reconnectSupported
.reconnectWaitMax(Int)Max seconds before reconnectSupported
.randomizationFactor(Double)Reconnect jitterSupported
.secure(Bool)Secure transportsSupported
.selfSigned(Bool)Self-signed certs (dev only)Ignored
.auth([String: Any])Authentication payloadIgnored

This project is a Swift Package Manager module that uses the Skip plugin to build the package for both iOS and Android.

The module can be tested using the standard swift test command or by running the test target for the macOS destination in Xcode, which will run the Swift tests as well as the transpiled Kotlin JUnit tests in the Robolectric Android simulation environment.

Parity testing can be performed with skip test, which will output a table of the test results for both platforms.