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 ofURLSessionTask
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 resum
ing 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,
- String
- Binary object (
Data
) - 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.