ios / 4 min read

How to use the URLSessionWebSocketTask in Swift. Post WWDC deep-dive review.

How to use the URLSessionWebSocketTask in Swift. Post WWDC deep-dive review.

With latest updates from WWDC 2019 WebSocket is now a first-class citizen in iOS, macOS, tvOS and watchOS. With iOS 13 , Apple introduced URLSessionWebSocketTask that was specifically designed for WebSockets. Intrigued, we decided to take it for a spin and find out what's cool about this new API.

URLSessionWebSocketTask

URLSession has a new set of methods for WebSocket operations:

@available(iOS 13.0, *)
open func webSocketTask(with url: URL) -> URLSessionWebSocketTask


@available(iOS 13.0, *)
open func webSocketTask(with url: URL, protocols: [String]) -> URLSessionWebSocketTask


@available(iOS 13.0, *)
open func webSocketTask(with request: URLRequest) -> URLSessionWebSocketTask

URLSessionWebSocketTask extends URLSessionTask, so you have the same base set of API that you use for regular URLSessionTask. It may seem reasonable at first, but in reality, 2-way real-time communication via WebSocket is different from typical HTTP request workflow. It would be much better to have a separate API designed specifically for WebSocket, but we have what we have :(

So let's create our first WebSocket connection. For testing purposes I'm going to use WebSocket echo server running on my machine. This server sends back every message it receives.

let urlSession = URLSession(configuration: .default)
let webSocketTask = urlSession.webSocketTask(with: "ws://0.0.0.0:8080/echo")

To open a connection we need to resume the webSocketTask

webSocketTask.resume()

Sending messages

Now it's time to send something. URLSessionWebSocketTask#send method accepts messages of type URLSessionWebSocketTask.Message. It is an enum with 2 cases:

public enum Message {
    case data(Data)
    case string(String)
}

We can only send text or binary data. We can't send low-level WebSocket frames or control messages. This is fine for most cases. You rarely need to work with custom WebSocket frames.

let message = URLSessionWebSocketTask.Message.string("Hello Socket)
webSocketTask.send(message) { error in
    if let error = error {                
        print("WebSocket sending error: \(error)")
    }
}

Echo server received "Hello Socket" and sent it back.

Receiving messages

To receive data coming from server, we need to use URLSessionWebSocketTask#receive method. This method accepts completion handler that receives a value of new Result type introduced in Swift 5.1

webSocketTask.receive { result in
    switch result {
    case .failure(let error):
        print("Failed to receive message: \(error)")
    case .success(let message):
        switch message {
        case .string(let text):
            print("Received text message: \(text)")
        case .data(let data):
            print("Received binary message: \(data)")
        @unknown default:
            fatalError()
        }                
    }
}

The value of the success case is the same URLSessionWebSocketTask.Message that we used to send messages.

The tricky part is this: receive method is only called once. If you want to receive another message, you need to call receive again.

func readMessage()  {
    webSocketTask.receive { result in
        switch result {
        case .failure(let error):
            print("Failed to receive message: \(error)")
        case .success(let message):
            switch message {
            case .string(let text):
                print("Received text message: \(text)")
            case .data(let data):
                print("Received binary message: \(data)")
            @unknown default:
                fatalError()
            }
            
            self.readMessage()
        }
    }
}

I have used dozens of WebSocket libraries across seven different programming languages, and this implementation is the strangest I have ever seen. Usually, you just set a callback on WebSocket, and it delivers all future messages to this callback. This is what you'd expect from a WebSocket API and, it's not clear to me why I have to create a new closure every time.

Ping Pongs

If your app is not sending messages over WebSocket with "acceptable" frequency, the server may drop your connection due to inactivity. Special ping-pong messages are used to solve this problem. You have to send them periodically (approximately every 10 seconds) to make sure the connection won't get killed by the server. You need to use webSocketTask.sendPing for that.

func ping() {
    webSocketTask.sendPing { (error) in
        if let error = error {
            print("Ping failed: \(error)")
        }
        self.scheduleNextPing()
    }
}

This method is very basic. It sends default ping messages, and notifies about its results. This API won't allow customizing ping message payload. Ping messages with custom payload can be used to deliver additional information to the server (statistics for example).

Closing connection

When we no longer need WebSocket connection, we still have to close it properly. URLSessionWebSocketTask has a separate WebSocket-specific cancel method.

open func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
...
func disconnect() {
    webSocketTask.cancel(with: .goingAway, reason: nil)
}

It accepts close code and optional reason payload. Close code is used to tell server why this connection is disconnecting. For example it may indicate different errors related to websocket or application protocols.

Observing connection state changes

Obviously, we need to know when our WebSocket connection is connected or disconnected and why.

To do that we need to use URLSessionWebSocketDelegate

@available(iOS 13.0, *)
public protocol URLSessionWebSocketDelegate : URLSessionTaskDelegate {
    
    optional func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?)
   
    optional func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
}

The API is pretty simple. It will notify you when connection was opened or closed with a close code and a reason from the server.

We need to pass this delegate to the URLSession constructor.

urlSession = URLSession(configuration: sessionConfiguration, delegate: delegate, delegateQueue: OperationQueue())

Again, Apple is trying to follow the existing URLSession architecture. To me it's weird why I need to share the same delegate for multiple websocket connections. Of course, we can still create an individual URLSession for each connection.

Is it ready for production?

It's only available in iOS 13 and, to be honest, URLSessionWebSocketTask definitely not the best WebSocket library available. You can use it if you need basic WebSocket functionality and if you don't need to support old iOS versions. In other cases, I would recommend using something more mature and properly designed like SwiftNIO from Apple or one of many open sources frameworks (Starscream for example).

Sample project

I have created a sample project with a wrapper class around the URLSessionWebSocketTask. It removes part of the boilerplate code and gives you an API similar to typical WebSocket library. This project has a SwiftNIO based echo server that you can use for testing. You can find it in this repo: https://github.com/appspector/URLSessionWebSocketTask

Summary

It is fascinating to see that iOS networking stack is evolving. It's definitely moving in the right direction. No, it's not ideal yet, but at least, you have your basic required functionality out of the box.

In our next article I will dig deeper into the world of low-level networking with Network.framework. Oh, the latest iOS update also brought WebSocket support to NVConnection.

So stay tuned and share this article, a lot of cool stuff is planned for 2019.


About Us

AppSpector is remote debugging and introspection tool for iOS and Android applications. With AppSpector you can debug your app running in the same room or on another continent. You can measure app performance, view CoreData and SQLite content, logs, network requests and many more in realtime. Just like you we have been struggling for years trying to find stupid mistakes and dreaming of a better native tools, finally we decided to build them. This is the instrument that you’ve been looking for.

Share: