iOS Development

Clean & Reusable WebSocket client in Swift

Harsh Kadiyaβ€’September 24, 2025β€’3 min read
πŸ‘ 3 views
Swift 6iOS DevelopmentSwiftLangConcurrencyApple DeveloperMobile DevelopmentwebSocket

Perfect question πŸ™Œ β€” right now, in the WebSocketManager, all the messages and events are just being printed inside the class. To make this reusable and clean, you should expose them through a protocol delegate so any class (like a ViewController) can handle messages/events.

Here’s the improved version with protocols + event handling:


πŸ“¦ WebSocketManager.swift

import Foundation

// MARK: - Protocol

protocol WebSocketManagerDelegate: AnyObject {
    func webSocketDidConnect(_ manager: WebSocketManager)
    func webSocketDidDisconnect(_ manager: WebSocketManager, code: URLSessionWebSocketTask.CloseCode, reason: Data?)
    func webSocketDidReceiveMessage(_ manager: WebSocketManager, text: String)
    func webSocketDidReceiveData(_ manager: WebSocketManager, data: Data)
    func webSocketDidReceiveError(_ manager: WebSocketManager, error: Error)
}

// MARK: - WebSocketManager

final class WebSocketManager: NSObject {
    
    private var webSocketTask: URLSessionWebSocketTask?
    private var urlSession: URLSession!
    private let url: URL
    
    weak var delegate: WebSocketManagerDelegate?
    
    init(url: URL) {
        self.url = url
        super.init()
        self.urlSession = URLSession(configuration: .default,
                                     delegate: self,
                                     delegateQueue: OperationQueue())
    }
    
    // MARK: - Public Methods
    
    func connect() {
        webSocketTask = urlSession.webSocketTask(with: url)
        webSocketTask?.resume()
        listen()
        sendPing()
    }
    
    func disconnect() {
        webSocketTask?.cancel(with: .goingAway, reason: nil)
    }
    
    func send(_ text: String) {
        webSocketTask?.send(.string(text)) { [weak self] error in
            if let error = error {
                self?.delegate?.webSocketDidReceiveError(self!, error: error)
            }
        }
    }
    
    // MARK: - Private Methods
    
    private func listen() {
        webSocketTask?.receive { [weak self] result in
            guard let self = self else { return }
            
            switch result {
            case .failure(let error):
                self.delegate?.webSocketDidReceiveError(self, error: error)
                
            case .success(let message):
                switch message {
                case .string(let text):
                    self.delegate?.webSocketDidReceiveMessage(self, text: text)
                case .data(let data):
                    self.delegate?.webSocketDidReceiveData(self, data: data)
                @unknown default:
                    break
                }
            }
            self.listen() // keep listening
        }
    }
    
    private func sendPing() {
        webSocketTask?.sendPing { [weak self] error in
            guard let self = self else { return }
            if let error = error {
                self.delegate?.webSocketDidReceiveError(self, error: error)
                return
            }
            DispatchQueue.global().asyncAfter(deadline: .now() + 10) {
                self.sendPing()
            }
        }
    }
}

// MARK: - URLSessionWebSocketDelegate

extension WebSocketManager: URLSessionWebSocketDelegate {
    func urlSession(_ session: URLSession,
                    webSocketTask: URLSessionWebSocketTask,
                    didOpenWithProtocol protocol: String?) {
        delegate?.webSocketDidConnect(self)
    }
    
    func urlSession(_ session: URLSession,
                    webSocketTask: URLSessionWebSocketTask,
                    didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
                    reason: Data?) {
        delegate?.webSocketDidDisconnect(self, code: closeCode, reason: reason)
    }
}

πŸš€ Usage Example (e.g. in a ViewController)

class ChatViewController: UIViewController, WebSocketManagerDelegate {
    
    private var socketManager: WebSocketManager!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        socketManager = WebSocketManager(url: URL(string: "wss://echo.websocket.events")!)
        socketManager.delegate = self
        socketManager.connect()
    }
    
    // MARK: - WebSocketManagerDelegate
    
    func webSocketDidConnect(_ manager: WebSocketManager) {
        print("βœ… Connected")
        manager.send("Hello from iOS πŸš€")
    }
    
    func webSocketDidDisconnect(_ manager: WebSocketManager, code: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        print("❌ Disconnected, code: \(code.rawValue)")
    }
    
    func webSocketDidReceiveMessage(_ manager: WebSocketManager, text: String) {
        print("πŸ“© Message: \(text)")
    }
    
    func webSocketDidReceiveData(_ manager: WebSocketManager, data: Data) {
        print("πŸ“¦ Data received: \(data.count) bytes")
    }
    
    func webSocketDidReceiveError(_ manager: WebSocketManager, error: Error) {
        print("⚠️ Error: \(error.localizedDescription)")
    }
}

βœ… Now your WebSocket is:

  • Encapsulated in a class
  • Event-driven with delegate callbacks
  • Reusable across multiple screens

About the Author

HK

Harsh Kadiya

Senior iOS & Flutter Developer

Subscribe to My Newsletter

Get the latest articles and insights delivered directly to your inbox