← Back to list

Writing a WebSocket Manager in iOS: Building an Echo Chat App from Scratch

How do you create a simple yet effective WebSocket Manager using Apple’s native WebSocket API?

Hasan Ali Siseci · 2025-11-28 12:33 · 7 claps · 4.9 min read paywalled
#websocket #websocket-api #echoapi #swiftui #swift-web-socket
Open on Medium ↗
Wiki topics: 📱 · Mobile Development 🔒 · Cybersecurity

Writing a WebSocket Manager in iOS: Building an Echo Chat App from Scratch

How do you create a simple yet effective WebSocket Manager using Apple’s native WebSocket API?

As iOS developers, when we want to build real-time applications, we rely on WebSocket technology. In this article, we will implement a practical WebSocket Manager using Apple’s URLSessionWebSocketTask API, introduced in iOS 13.

We will focus on the core functions of a WebSocket Manager: establishing a connection, listening for messages, handling ping, and managing errors.

What Is an Echo WebSocket Service?

For testing purposes, we will use the wss://echo.websocket.org/ service. The Echo WebSocket service is very helpful during the learning phase.

How does the Echo service work?

It sends back exactly the same message you send.

  • No registration or authentication required
  • Available 24/7
  • Completely free
  • Real-time responses (usually <100ms)

Why do we use an Echo service?

// Your message
webSocketManager.sendMessage("Merhaba WebSocket!")

// Response from Echo
"Merhaba WebSocket!" // Aynı mesaj geri döner

This allows you to:

  • Test whether the WebSocket connection is successfully established
  • Confirm send/receive operations
  • Verify message formatting
  • Measure network latency
  • Test connection stability

Real-world example: After learning WebSocket fundamentals with the Echo service, transitioning to real APIs like Coinbase becomes much easier. The connection, message handling, and state management logic remain the same — only URLs, message formats, and data types change.

Core Responsibilities of a WebSocket Manager

A WebSocket Manager should handle:

  • connect() — Create a WebSocket connection
  • disconnect() — Close the connection cleanly
  • sendMessage() — Send a message
  • listen() — Listen for incoming messages
  • sendPing() — Validate connection health

Structure of the Manager Class

import SwiftUI

class WebSocketManager: NSObject, ObservableObject {
    // MARK: - Published Properties (SwiftUI için)
    @Published var messages: [ChatMessage] = []
    @Published var connectionStatus: ConnectionStatus = .disconnected
    @Published var isConnecting = false

    // MARK: - Private Properties
    private var webSocketTask: URLSessionWebSocketTask?
    private let urlSession = URLSession(configuration: .default)
}

Critical Points:

  • @Published enables reactive SwiftUI updates
  • URLSessionWebSocketTask is Apple’s native WebSocket implementation

connect() — Bağlantı Kurma Süreci

func connect() {
    // 1. Önceki bağlantıyı temizle
    disconnect()

    // 2. URL doğrulama
    guard let url = URL(string: "wss://echo.websocket.org/") else {
        connectionStatus = .error("Geçersiz URL")
        return
    }

    // 3. Connection state güncelle
    connectionStatus = .connecting
    isConnecting = true

    // 4. WebSocketTask oluştur ve başlat
    webSocketTask = urlSession.webSocketTask(with: url)
    webSocketTask?.resume()

    // 5. Mesaj dinleme döngüsünü başlat
    listen()

    // 6. Bağlantı sağlığını test et
    sendPing()
}

Step-by-Step Explanation

  1. Cleanup: disconnect() removes any previous connection to avoid memory leaks.
  2. URL Validation: Ensures the URL is valid.
  3. State Management: Updates UI states (connectionStatus, isConnecting).
  4. Task Creation: Creates a WebSocket task and starts it.
  5. Recursive Listening: Start listening for messages.
  6. Health Check: Send a ping to verify the connection.

disconnect() — Clean Shutdown

func disconnect() {
    webSocketTask?.cancel(with: .goingAway, reason: nil)
    webSocketTask = nil
    connectionStatus = .disconnected
    isConnecting = false
}

Why .goingAway?

  • Standard WebSocket close code indicating a normal client-initiated shutdown
  • Server won’t attempt reconnection
  • RFC-compliant clean close

listen() — Recursive Message Listening

private func listen() {
    webSocketTask?.receive { [weak self] result in
        switch result {
        case .success(let message):
            DispatchQueue.main.async {
                // Connection başarılı olduğunu belirt
                self?.connectionStatus = .connected
                self?.isConnecting = false

                // Mesaj tipine göre işle
                switch message {
                case .string(let text):
                    self?.addMessage(ChatMessage(text: text, isOutgoing: false))
                case .data(let data):
                    let text = String(data: data, encoding: .utf8) ?? "Binary data alındı"
                    self?.addMessage(ChatMessage(text: text, isOutgoing: false))
                @unknown default:
                    break
                }
            }

            // 🚨 KRİTİK: Bir sonraki mesaj için recursively call
            self?.listen()

        case .failure(let error):
            DispatchQueue.main.async {
                self?.connectionStatus = .error(error.localizedDescription)
                self?.isConnecting = false
                self?.addMessage(ChatMessage(text: "Bağlantı hatası: \(error.localizedDescription)", isOutgoing: false, isError: true))
            }
        }
    }
}

Most Important Point: Recursive listen() call

WebSockets do NOT work like HTTP request-response. receive() listens only for one message. Therefore, we must call listen() again after every message.

Common mistake:

// ❌ WRONG – listens only once
private func listenOnce() {
    webSocketTask?.receive { result in
       // Processes message but does not listen again
    }
}

Memory safety: [weak self] prevents retain cycles. Thread safety: UI updates must be on the main thread.

sendMessage() — Sending Messages

func sendMessage(_ text: String) {
    // 1. WebSocketTask control
    guard let webSocketTask = webSocketTask else {
        addMessage(ChatMessage(text: "Bağlantı yok!", isOutgoing: false, isError: true))
        return
    }

    // 2. String to URLSessionWebSocketTask.Message type
    let message = URLSessionWebSocketTask.Message.string(text)

    // 3. Async send operation
    webSocketTask.send(message) { [weak self] error in
        DispatchQueue.main.async {
            if let error = error {
                // Send unsuccess
                self?.addMessage(ChatMessage(text: "Gönderme hatası: \(error.localizedDescription)", isOutgoing: false, isError: true))
            } else {
                // Send success, append to list
                self?.addMessage(ChatMessage(text: text, isOutgoing: true))
            }
        }
    }
}

Key Details:

  1. Prevent sending without a connection
  2. Convert string to WebSocket message format
  3. Handle async callback
  4. Display sent messages instantly

sendPing() — Testing the Connection

private func sendPing() {
    webSocketTask?.sendPing { [weak self] error in
        DispatchQueue.main.async {
            if let error = error {
                self?.connectionStatus = .error("Ping error: \(error.localizedDescription)")
                self?.isConnecting = false
            } else {
                self?.connectionStatus = .connected
                self?.isConnecting = false
                self?.addMessage(ChatMessage(text: "Connect to Echo WebSocket! Send a message to see it returned.", isOutgoing: false, isSystem: true))
            }
        }
    }
}

Ping/Pong Mechanism

Used to verify if the connection is still alive.

Goals:

  • Verify established connection
  • Detect network issues early
  • Prevent NAT/firewall timeouts

Ping is sent → server replies with pong automatically.

ChatMessage Model

struct ChatMessage: Identifiable {
    let id = UUID()
    let text: String
    let timestamp = Date()
    let isOutgoing: Bool
    let isError: Bool
    let isSystem: Bool

    init(text: String, isOutgoing: Bool, isError: Bool = false, isSystem: Bool = false) {
        self.text = text
        self.isOutgoing = isOutgoing
        self.isError = isError
        self.isSystem = isSystem
    }
}

Message Types:

  • isOutgoing = true — user messages (blue bubble)
  • isError = true — error messages (red bubble)
  • isSystem = true — system notifications (orange bubble)

Minimal UI Implementation

struct ContentView: View {
    @StateObject private var webSocketManager = WebSocketManager()
    @State private var messageText = ""

    var body: some View {
        VStack {
            // Connection Status
            Text(webSocketManager.connectionStatus.displayText)
                .padding()

            // Messages
            List(webSocketManager.messages.suffix(10)) { message in
                HStack {
                    Text(message.isOutgoing ? "➡️" : "⬅️")
                    Text(message.text)
                        .foregroundColor(message.isError ? .red : .primary)
                }
            }

            // Message Input
            HStack {
                TextField("Message", text: $messageText)
                    .textFieldStyle(.roundedBorder)

                Button("Send") {
                    webSocketManager.sendMessage(messageText)
                    messageText = ""
                }
                .disabled(messageText.isEmpty)
            }
            .padding()
        }
        .onAppear { webSocketManager.connect() }
        .onDisappear { webSocketManager.disconnect() }
    }
}

Things to Pay Attention To

1. Recursive Listen Pattern

// ✅ Doğru
self?.listen() // Her mesajdan sonra
// ❌ Yanlış  
// listen() çağrısını unutmak

2. Memory Management

// ✅ Doğru
{ [weak self] in ... }
// ❌ Yanlış
{ self in ... } // Retain cycle

3. Thread Safety

// ✅ Doğru
DispatchQueue.main.async { 
    self?.connectionStatus = .connected 
}
// ❌ Yanlış
self?.connectionStatus = .connected // Background thread

4. Connection Cleanup

// ✅ True
webSocketTask?.cancel(with: .goingAway, reason: nil)
webSocketTask = nil
// ❌ False
// Task'ı nil'lemeden önce cancel etmemek

Test Scenarios

Using the Echo WebSocket service, you can test:

  • Normal messaging (“Hello” → “Hello”)
  • Connection state transitions
  • Error handling by enabling/disabling internet
  • Ping success
  • Message roundtrip latency

Next Steps

After this basic implementation, you can extend functionality with:

  • Auto-reconnection
  • Offline message queue
  • Heartbeat timer
  • Connection metrics (latency, uptime)

Conclusion

Fundamental principles of building a WebSocket Manager in iOS:

connect() — URL validation, task creation, start listening ✅ listen() — Continuous listening with recursion ✅ sendMessage() — Safe async send ✅ sendPing() — Connection health check ✅ disconnect() — Clean resource teardown

With this foundation, you can build production-ready WebSocket-based applications.

You can access the GitHub repository for this project here.

Happy coding!


메타데이터
post_id
ba2d0093478b
slug
writing-a-websocket-manager-in-ios-building-an-echo-chat-app-from-scratch-ba2d0093478b
url
https://medium.com/@hasanalidev/writing-a-websocket-manager-in-ios-building-an-echo-chat-app-from-scratch-ba2d0093478b
canonical_url
https://medium.com/@hasanalidev/writing-a-websocket-manager-in-ios-building-an-echo-chat-app-from-scratch-ba2d0093478b
author_url
https://medium.com/@hasanalidev
status
ok
fetched_at
2026-07-25 17:20:28