WebSockets on iOS using URLSessionWebSocketTask

iOS added support for WebSocket starting iOS 13 and they announced it in WWDC2019. Before that, developers had to rely on third-party libraries such as Starscream or SwiftWebSocket. Now with native support, it all becomes quite easy to add on without adding third-party overhead.

Previously, I wrote about using WebSockets on iOS using third-party libraries. In this post, I am going to write how you can achieve the same thing using the native approach.

To understand WebSockets on iOS, we will go step-by-step and understand how each part of the setup works.

iOS API

iOS allows using web sockets using URLSessionWebSocketTask API introduced in iOS 13. To quote Apple on this API,

URLSessionWebSocketTask is a concrete subclass of URLSessionTask that provides a message-oriented transport protocol over TCP and TLS in the form of WebSocket framing. It follows the WebSocket Protocol defined in RFC 6455.

Web Sockets Endpoint

To test our changes, we will use a known WebSocket endpoint that provides capabilities such as registering for values and periodically updating the client with values over time. This API is provided by BUX, but you can use any API that provides WebSocket support.

wss://rtf.beta.getbux.com/subscriptions/me

Please note that this is not a freely available API. You will need an authentication token to send request to this endpoint. If possible, I will recommend running a local WebSocket server or using already available WebSocket API

Opening a WebSocket

To start sending or receiving messages over WebSocket, we first need to open the web socket connection. We will use URLSessionWebSocketTask API to achieve this,


func openWebSocket() {
	let urlString = "wss://rtf.beta.getbux.com/subscriptions/me"
	if let url = URL(string: urlString) {
        var request = URLRequest(url: url)
        let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
        let webSocket = session.webSocketTask(with: request)
        webSocket?.resume()
	}
}

Please note that we create our own URLSession object and set the current class as a delegate to URLSession which conforms to URLSessionWebSocketDelegate protocol. Setting this delegate is optional but can be used to get a callback when the socket opens or closes so that you can log analytics or analyze debug information.

Let's implement this delegate for our current class SocketNetworkService,


extension SocketNetworkService: URLSessionWebSocketDelegate {
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
        print("Web socket opened")
        isOpened = true
    }

    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        print("Web socket closed")
        isOpened = false
    }
}

We will start receiving messages immediately after opening a web socket, but to receive them, we will need to set up using  receive API on URLSessionWebSocketTask instance.

Sending a message

We can send a message to the socket endpoint using send API on URLSessionWebSocketTask instance. It accepts an object of type URLSessionWebSocketTask.Message and provides completion handler with optional Error object which indicates whether send operation resulted in an error or not.

Since URLSessionWebSocketTask.Message is enum with String and Data cases, you can use send API to send messages in either String or Binary data format.

String:


webSocket.send(URLSessionWebSocketTask.Message.string("Hello")) { [weak self] error in
    if let error = error {
        print("Failed with Error \(error.localizedDescription)")
    } else {
        // no-op
    }
}

Data:


webSocket.send(URLSessionWebSocketTask.Message.data("Hello".data(using: .utf8)!)) { [weak self] error in
    if let error = error {
        print("Failed with Error \(error.localizedDescription)")
    } else {
        self?.closeSocket()
    }
}

Receiving messages

To receive messages over WebSocket, we will use the receive API. Let's see how it is set up,

public func receive(completionHandler: @escaping (Result<URLSessionWebSocketTask.Message, Error>) -> Void)

Receive method provides input values through completionHandler which returns object through Result object which can either represent failure or success.


var request = URLRequest(url: URL("wss://rtf.beta.getbux.com/subscriptions/me")!)

let webSocket = URLSession.shared.webSocketTask(with: request)

webSocket.resume()

webSocket.receive(completionHandler: { result in   
    
    switch result {
    case .failure(let error):
        print(error.localizedDescription)
    case .success(let message):
        switch message {
        case .string(let messageString):
            print(messageString)
        case .data(let data):
            print(data.description)
        default:
			print("Unknown type received from WebSocket")
        }
    }
})

Right after resuming socket, we set up the callback with receive method. The result represents two cases, success, and failure. If it's a failure, we will log the appropriate message and move on. If successful, there are three cases,

  1. String
  2. Binary object (Data)
  3. Default type

Depending on which format you expect the data back, you can further process this result.

Caveat for Receive Method!!!

Apple provided receieve method is quirky in the sense that after receiving a single message in its callback it automatically unregisters from receiving further messages. As a workaround, you need to re-register to receive callback every time after you receive a message. It sounds roundabout and unnecessary, but this is how this API currently works.

To achieve this, let's wrap the above code into a function, and let's call it again after receiving a message


func receiveMessage() {

	if !isOpened {
    	openWebSocket()
    }

    webSocket.receive(completionHandler: { [weak self] result in
        
        switch result {
        case .failure(let error):
            print(error.localizedDescription)
        case .success(let message):
            switch message {
            case .string(let messageString):
                print(messageString)
            case .data(let data):
                print(data.description)
            default:
                print("Unknown type received from WebSocket")
            }
        }
        self?.receiveMessage()
    })
}

func openWebSocket() {
	let urlString = "wss://rtf.beta.getbux.com/subscriptions/me"
	if let url = URL(string: urlString) {
        var request = URLRequest(url: url)
        let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
        let webSocket = session.webSocketTask(with: request)
        webSocket?.resume()
        isOpened = true
	}
}
Never call receive method just once unless you want to receive message only once. Next time you need value over socket, you need to set it up again. If receive method returns failure for some reason, you may depending on your use-case stop there and instead close the connection

Keeping connection alive

The web socket will close the connection if it's idle for a long time. If you need to keep it open for an indefinite amount of time, you can periodically send a ping to keep it alive,


let timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] timer in
    self?.webSocket?.sendPing(pongReceiveHandler: { error in
        if let error = error {
            print("Failed with Error \(error.localizedDescription)")
        } else {
            // no-op
        }
    })
}
timer.fire()

Closing a WebSocket

Keeping web sockets open for a long time or when it is unnecessary takes up all the necessary resources and may cause an extra drain on battery life and network data. If not used, especially when the user navigates away from the screen that needs web socket data, we can safely close it out.

To close the socket connection, we will use cancel API on URLSessionWebSocketTask instance.


func closeSocket() {
    webSocket.cancel(with: .goingAway, reason: nil)
    webSocket = nil
    isOpened = false
}

Final Thoughts

What do I think about this API? Well, for starters, Apple has been terribly late to publish Web socket API on iOS especially when it's been live in other languages for years. Despite the delay, it isn't as near impressive as I would like it to be.

Second, I don't understand why it tries to extend existing URLSession API to extend web socket support. This API was primarily used for HTTP requests for years and it's intuitive to developers. Having mixed both of them especially WebSockets work completely different than regular HTTP requests add a lot of confusion. I think it would've been beneficial for Apple too since it could continue developing both standards parallel without stepping on each other's toes.

Third, having the need to call receive method every time after socket receive the message is redundant and useless. Not to mention other performance hits which I haven't measured yet. Other socket libraries such as Starscream and SwiftWebSocket allows developers to set up this callback just once and it will keep receiving messages as long as the connection is open.

If I leave these concerns aside, it's promising to see this API on iOS. Hopefully, Apple will do a good job of maintaining it in the long term and addressing some of the inconveniences here. Next time I have to implement web sockets, Apple will be my first choice as we are trying to reduce dependency on third-party libraries unless we have a strong reason to do so.

If you have any thoughts or concerns around this API, I would love to hear from you too. You can always message me or directly reach out on Twitter @jayeshkawli

This is the second article in the series of articles for WebSocket. I have already published first one on how to use WebSocket on iOS using third-party APIs. In the next article, I will write on how you can spin off your own WebSocket server using Network module on iOS.

References:

WWDC2019 - Advances in Networking, Part 1

How to use the URLSessionWebSocketTask in Swift