Community-maintained Agent Skills for complete Swift and Apple-platform app delivery.
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Medium
Suggest reviewing before use
Use this reference when building real-time bidirectional WebSocket connections, handling reconnections, structured concurrency streaming, and Codable messaging in iOS.
URLSessionWebSocketTask provides native WebSocket support without
third-party libraries. Available since iOS 13. WebSockets use ws: or wss:
URLs and are foreground/default-session realtime networking; background
URLSession configuration does not make a WebSocket connection durable after
suspension.
@available(iOS 15.0, *)
final class WebSocketConnection: Sendable {
private let task: URLSessionWebSocketTask
init(url: URL, session: URLSession = .shared) {
self.task = session.webSocketTask(with: url)
}
func connect() {
task.resume()
}
func disconnect(reason: String? = nil) {
task.cancel(with: .normalClosure, reason: reason?.data(using: .utf8))
}
func send(_ message: URLSessionWebSocketTask.Message) async throws {
try await task.send(message)
}
func send(text: String) async throws {
try await task.send(.string(text))
}
func send(data: Data) async throws {
try await task.send(.data(data))
}
func receive() async throws -> URLSessionWebSocketTask.Message {
try await task.receive()
}
}The key pattern: run a receive loop as an async task that yields
messages through an AsyncStream. This integrates naturally with
structured concurrency.
@available(iOS 15.0, *)
actor WebSocketManager {
private var task: URLSessionWebSocketTask?
private var receiveTask: Task<Void, Never>?
private let session: URLSession
private let url: URL
enum Event: Sendable {
case connected
case text(String)
case data(Data)
case disconnected(URLSessionWebSocketTask.CloseCode, Data?)
case error(Error)
}
init(url: URL, session: URLSession = .shared) {
self.url = url
self.session = session
}
/// Returns a stream of WebSocket events. Call `connect()` to start.
func events() -> AsyncStream<Event> {
AsyncStream { continuation in
let wsTask = session.webSocketTask(with: url)
self.task = wsTask
wsTask.resume()
continuation.yield(.connected)
// Start the receive loop
self.receiveTask = Task { [weak self] in
await self?.receiveLoop(continuation: continuation)
}
continuation.onTermination = { _ in
Task { [weak self] in
await self?.disconnect()
}
}
}
}
private func receiveLoop(continuation: AsyncStream<Event>.Continuation) async {
guard let task else { return }
while !Task.isCancelled {
do {
let message = try await task.receive()
switch message {
case .string(let text):
continuation.yield(.text(text))
case .data(let data):
continuation.yield(.data(data))
@unknown default:
break
}
} catch {
// The receive threw -- connection closed or failed
let closeCode = task.closeCode
let closeReason = task.closeReason
if closeCode == .invalid {
// Unexpected disconnection
continuation.yield(.error(error))
} else {
continuation.yield(.disconnected(closeCode, closeReason))
}
continuation.finish()
return
}
}
}
func send(text: String) async throws {
try await task?.send(.string(text))
}
func send(data: Data) async throws {
try await task?.send(.data(data))
}
func disconnect() {
receiveTask?.cancel()
receiveTask = nil
task?.cancel(with: .normalClosure, reason: nil)
task = nil
}
/// Send periodic pings to keep the connection alive
func startPinging(interval: Duration = .seconds(30)) {
Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: interval)
guard let self else { return }
await self.ping()
}
}
}
private func ping() {
task?.sendPing { error in
if let error {
// Connection may be dead
print("Ping failed: \(error)")
}
}
}
}@MainActor
@Observable final class ChatStore {
var messages: [ChatMessage] = []
var connectionState: ConnectionState = .disconnected
enum ConnectionState { case disconnected, connecting, connected }
private let wsManager: WebSocketManager
private var eventTask: Task<Void, Never>?
init(url: URL) {
self.wsManager = WebSocketManager(url: url)
}
func connect() async {
connectionState = .connecting
let stream = await wsManager.events()
eventTask = Task {
for await event in stream {
await handleEvent(event)
}
}
}
func sendMessage(_ text: String) async {
do {
try await wsManager.send(text: text)
messages.append(ChatMessage(text: text, isOutgoing: true))
} catch {
// Handle send failure
}
}
func disconnect() async {
eventTask?.cancel()
eventTask = nil
await wsManager.disconnect()
connectionState = .disconnected
}
private func handleEvent(_ event: WebSocketManager.Event) async {
switch event {
case .connected:
connectionState = .connected
case .text(let text):
messages.append(ChatMessage(text: text, isOutgoing: false))
case .data(let data):
if let text = String(data: data, encoding: .utf8) {
messages.append(ChatMessage(text: text, isOutgoing: false))
}
case .disconnected:
connectionState = .disconnected
case .error:
connectionState = .disconnected
// Optionally trigger reconnection
}
}
}struct ChatView: View {
@State var store: ChatStore
var body: some View {
List(store.messages) { message in
ChatBubble(message: message)
}
.task { await store.connect() }
.onDisappear { Task { await store.disconnect() } }
}
}Network drops happen. A robust WebSocket client must reconnect automatically with exponential backoff.
@available(iOS 15.0, *)
actor ReconnectingWebSocket {
private let url: URL
private let session: URLSession
private let maxReconnectAttempts: Int
private let initialDelay: Duration
private let maxDelay: Duration
private var currentManager: WebSocketManager?
private var reconnectAttempts = 0
private var isIntentionalDisconnect = false
init(
url: URL,
session: URLSession = .shared,
maxReconnectAttempts: Int = 10,
initialDelay: Duration = .seconds(1),
maxDelay: Duration = .seconds(60)
) {
self.url = url
self.session = session
self.maxReconnectAttempts = maxReconnectAttempts
self.initialDelay = initialDelay
self.maxDelay = maxDelay
}
/// Returns a stream that automatically reconnects on disconnection.
func events() -> AsyncStream<WebSocketManager.Event> {
AsyncStream { continuation in
Task {
await connectWithReconnection(continuation: continuation)
}
continuation.onTermination = { _ in
Task { [weak self] in
await self?.intentionalDisconnect()
}
}
}
}
private func connectWithReconnection(
continuation: AsyncStream<WebSocketManager.Event>.Continuation
) async {
while !isIntentionalDisconnect && reconnectAttempts < maxReconnectAttempts {
guard !Task.isCancelled else { break }
let manager = WebSocketManager(url: url, session: session)
currentManager = manager
let stream = await manager.events()
for await event in stream {
switch event {
case .connected:
reconnectAttempts = 0 // Reset on successful connection
continuation.yield(event)
case .error, .disconnected:
continuation.yield(event)
default:
continuation.yield(event)
}
}
// Stream ended -- attempt reconnection unless intentional
guard !isIntentionalDisconnect, !Task.isCancelled else { break }
reconnectAttempts += 1
let delay = calculateBackoff()
do {
try await Task.sleep(for: delay)
} catch {
break // Cancelled during sleep
}
}
continuation.finish()
}
private func calculateBackoff() -> Duration {
let base = Double(initialDelay.components.seconds) * pow(2.0, Double(reconnectAttempts - 1))
let capped = min(base, Double(maxDelay.components.seconds))
let jitter = Double.random(in: 0...(capped * 0.25))
return .seconds(capped + jitter)
}
func send(text: String) async throws {
try await currentManager?.send(text: text)
}
func send(data: Data) async throws {
try await currentManager?.send(data: data)
}
private func intentionalDisconnect() {
isIntentionalDisconnect = true
Task {
await currentManager?.disconnect()
}
}
}For typed message protocols (common in chat, gaming, real-time apps), decode/encode messages automatically.
protocol WebSocketMessage: Codable, Sendable {
static var messageType: String { get }
}
struct TypedWebSocketTransport {
private let manager: WebSocketManager
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
init(manager: WebSocketManager) {
self.manager = manager
}
func send<T: WebSocketMessage>(_ message: T) async throws {
let envelope = MessageEnvelope(
type: T.messageType,
payload: try encoder.encode(message)
)
let data = try encoder.encode(envelope)
try await manager.send(data: data)
}
/// Typed event stream that decodes known message types
func typedEvents() async -> AsyncStream<DecodedEvent> {
let rawEvents = await manager.events()
return AsyncStream { continuation in
Task {
for await event in rawEvents {
switch event {
case .data(let data):
if let envelope = try? decoder.decode(MessageEnvelope.self, from: data) {
continuation.yield(.message(type: envelope.type, payload: envelope.payload))
}
case .text(let text):
if let data = text.data(using: .utf8),
let envelope = try? decoder.decode(MessageEnvelope.self, from: data) {
continuation.yield(.message(type: envelope.type, payload: envelope.payload))
}
case .connected:
continuation.yield(.connected)
case .disconnected(let code, _):
continuation.yield(.disconnected(code))
case .error(let error):
continuation.yield(.error(error))
}
}
continuation.finish()
}
}
}
enum DecodedEvent: Sendable {
case connected
case message(type: String, payload: Data)
case disconnected(URLSessionWebSocketTask.CloseCode)
case error(Error)
}
private struct MessageEnvelope: Codable, Sendable {
let type: String
let payload: Data
}
}WebSocket connections often require authentication via a token in the initial handshake (either as a query parameter or a custom header).
func authenticatedWebSocket(
baseURL: URL,
token: String
) -> URLSessionWebSocketTask {
// Option 1: Token as query parameter
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else {
preconditionFailure("Invalid URL components for: \(baseURL)")
}
components.queryItems = [URLQueryItem(name: "token", value: token)]
guard let authenticatedURL = components.url else {
preconditionFailure("Failed to construct URL from components")
}
let task = URLSession.shared.webSocketTask(with: authenticatedURL)
// Option 2: Token as custom header (use URLRequest)
var request = URLRequest(url: baseURL)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let taskWithHeader = URLSession.shared.webSocketTask(with: request)
return taskWithHeader
}Prefer the header approach when the server supports it. Query parameters may appear in server access logs, which is a security concern for tokens.
// Request a specific subprotocol (e.g., graphql-ws)
let task = URLSession.shared.webSocketTask(
with: url,
protocols: ["graphql-transport-ws"]
)
task.resume()
// After connection, verify the negotiated protocol
// via the URLSessionWebSocketDelegateextension WebSocketConnection: URLSessionWebSocketDelegate {
nonisolated func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?
) {
print("Connected with protocol: \(`protocol` ?? "none")")
}
nonisolated func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?
) {
let reasonString = reason.flatMap { String(data: $0, encoding: .utf8) }
print("Closed: \(closeCode) - \(reasonString ?? "no reason")")
}
}.tessl-plugin
skills
accessorysetupkit
references
activitykit
adattributionkit
references
alarmkit
references
app-clips
app-intents
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
audioaccessorykit
references
authentication
references
avfoundation-audio
references
avkit
background-processing
references
browserenginekit
callkit
references
carplay
cloudkit
contacts-framework
references
core-bluetooth
references
core-data
core-haptics
references
core-motion
references
core-nfc
references
coreml
references
cryptokit
cryptotokenkit
debugging-instruments
device-integrity
references
energykit
references
eventkit
family-controls
references
financekit
focus-engine
gamekit
git-branching-workflow
references
healthkit
references
homekit
references
ios-accessibility
ios-app-workflow
references
ios-ettrace-performance
ios-localization
ios-memgraph-analysis
ios-networking
ios-simulator
references
metrickit
references
musickit
references
natural-language
paperkit
references
passkit
references
pdfkit
pencilkit
references
permissionkit
references
photokit
push-notifications
quicklook
references
realitykit
references
relevancekit
references
scenekit
sensorkit
shazamkit
references
speech-recognition
references
spritekit
storekit
swift-api-design-guidelines
swift-architecture
references
swift-charts
swift-codable
references
swift-code-review
swift-concurrency
swift-formatstyle
references
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-responsive-layout
swiftui-uikit-interop
swiftui-webkit
tabletopkit
tipkit
vision-framework
weatherkit
references