Skip to Content
Course content

198: WebSocket Communication in Swift

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been relying on standard REST APIs, you're used to the "request-response" dance: your app asks for data, the server sends it, and the connection closes. That works great for a user profile page, but it's a nightmare for something like a live sports score tracker. You can't have your app polling the server every second just to see if a goal was scored; it's a waste of battery and server resources. This is where WebSockets come in, allowing a persistent, two-way street between the client and the server.

The "One-and-Done" Trap

When I first started using URLSessionWebSocketTask, I fell into a common trap. I treated the receive method like a delegate or a notification listener. I'd call receive once, handle the incoming message in the completion handler, and assume the system would keep listening for the next one. It looked like this:

let webSocketTask = URLSession.shared.webSocketTask(with: url)
webSocketTask.resume()

webSocketTask.receive { result in
    switch result {
    case .success(let message):
        print("Received score update: \(message)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

The problem? This code works exactly once. You'll get the first score update of the game, and then the connection will just sit there in silence. The receive method in Swift is designed to deliver a single message. Once that completion handler executes, the "listener" is gone. If you're building a real-time app, this is a critical failure—your users will see the first update and then think the app has frozen.

Closing the Loop for Continuous Streams

To actually maintain a stream, you have to implement a recursive listening pattern. I like to think of it as "re-arming the trap." The moment you finish processing a message, you must immediately tell the task to listen for the next one. This creates a continuous loop that keeps the data flowing without manually polling.

Here is how I usually structure this to ensure it doesn't crash or leak memory:

class ScoreTracker {
    private var webSocketTask: URLSessionWebSocketTask?

    func connect() {
        let url = URL(string: "wss://sports-api.example.com/scores")!
        webSocketTask = URLSession.shared.webSocketTask(with: url)
        webSocketTask?.resume()
        listen()
    }

    private func listen() {
        webSocketTask?.receive { [weak self] result in
            switch result {
            case .success(let message):
                self?.handleMessage(message)
                // Re-arm the listener here
                self?.listen() 
            case .failure(let error):
                print("Connection lost: \(error)")
                self?.handleDisconnect()
            }
        }
    }

    private func handleMessage(_ message: URLSessionWebSocketTask.Message) {
        // Handle the actual data (string or binary)
    }
    
    private func handleDisconnect() {
        // Implement exponential backoff retry logic here
    }
}

Notice the [weak self]. Because the receive completion handler is escaping, you're creating a strong reference cycle if you aren't careful. If the user navigates away from the score screen and you don't have weak self, your ScoreTracker instance will live forever in memory, continuing to listen to a socket that no longer has a UI to update.

Managing Lifecycle and Silent Drops

There's a hidden cost to this approach: the "Silent Drop." WebSockets can feel connected when they aren't. A load balancer or a cellular tower might kill the TCP connection without sending a formal "close" frame. Your app thinks it's still listening, but no data is coming through.

To fight this, you need to implement a heartbeat (or "ping"). URLSessionWebSocketTask has a built-in sendPing method. I typically set up a timer that sends a ping every 30 seconds. If the ping fails or the server doesn't respond, you know the connection is dead and you can trigger your reconnection logic. It's an extra bit of boilerplate, but it's the difference between an app that feels professional and one that requires a hard restart every time the user switches from Wi-Fi to 5G.




📋 Practical Task

Build a Live Cryptocurrency Price Ticker

Your goal is to create a CryptoPriceListener class that connects to a public WebSocket API (such as the Binance Public WebSocket API) to track the price of Bitcoin (BTC) in real-time.

Requirements:

  • Implement a recursive receive loop so the price updates continuously without stopping after the first message.
  • Use [weak self] within the completion handler to prevent memory leaks.
  • Implement a sendPing()` method that is called every 20 seconds via a Timer to keep the connection alive.
  • Handle the incoming JSON message to extract and print only the current price string to the console.
  • Ensure the WebSocket task is properly cancelled in a deinit method to clean up resources.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.