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?
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 connectiondisconnect()— Close the connection cleanlysendMessage()— Send a messagelisten()— Listen for incoming messagessendPing()— 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:
@Publishedenables reactive SwiftUI updatesURLSessionWebSocketTaskis 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
- Cleanup:
disconnect()removes any previous connection to avoid memory leaks. - URL Validation: Ensures the URL is valid.
- State Management: Updates UI states (
connectionStatus,isConnecting). - Task Creation: Creates a WebSocket task and starts it.
- Recursive Listening: Start listening for messages.
- 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:
- Prevent sending without a connection
- Convert string to WebSocket message format
- Handle async callback
- 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