Waldo sessions now support scripting! – Learn more
App Development

Getting Started With WebSockets in iOS

Juan Reyes
Juan Reyes
Getting Started With WebSockets in iOS
January 24, 2023
8
min read

8WebSockets are a powerful tool for building real-time applications because they allow for bidirectional communication between the client and server. This article will explore how to use WebSockets in iOS, specifically with the popular networking library Socket.IO. We'll start by defining what WebSockets are and why they're important. Then we'll explain how they establish a connection by illustrating a simple implementation example with an iOS chat client. Next, we'll explore the advantages and disadvantages of using WebSockets. Finally, we'll list the best practices to follow when implementing WebSockets.

Before delving into the implementation specifics, let's first comprehend the fundamentals of WebSockets and how they vary from conventional HTTP communication.

Understanding WebSockets

The HTTP request-response model has traditionally been the foundation for client-server communication. The server receives a request from the client and replies with a response. This has several restrictions but generally works well.

The first thing to note is that most HTTP requests are unidirectional, meaning that while the client can give the server a request, the server cannot push data to the client without the client first making a request. Applications that demand real-time updates, such as chat programs or multiplayer games, may have issues with this.

WebSockets were designed to address this limitation by providing a persistent, bidirectional connection between the client and server. Once a WebSocket connection has been made, neither side needs to wait for a request-response cycle before sending data to the other. This enables updates and real-time communication between the server and the client.

Setting Up a WebSocket Connection

To establish a WebSocket connection, the client sends a handshake request to the server, asking to upgrade the connection from HTTP to WebSockets. The server then responds with an HTTP 101 status code, indicating that the upgrade has been successful and is now a WebSocket connection.

After the connection is established, either party can send data to the other using the WebSocket protocol, which is based on a simple frame structure. Each frame contains a small amount of data and a few bytes of metadata, such as the frame type and the payload length.

Using WebSockets in iOS

Now that we have a basic understanding of WebSockets, let's look at how to use them in an iOS application. We'll use the Socket.IO library, which provides a convenient interface for working with WebSockets in iOS.

To get started, we first need to install the Socket.IO library.

To install the library, all you have to do is click on File -> Add Packages, then add the following URL to the search bar on the top right:


https://github.com/socketio/socket.io-client-swift
 Websockets in iOs screenshot 1

Finally, click on Add Package.

With the Socket.IO library installed, we can now use WebSockets in our iOS application. Let's start by going through the fundamentals of a basic connection implementation.

Connecting to a WebSocket Server

To connect to a WebSocket server, we first need to create a SocketIOClient object and specify the server URL. We can then call the connect method to establish a connection:


let socket = SocketIOClient(socketURL: URL(string: "http://localhost:3000")!) socket.connect()

Once the connection is established, we can start sending and receiving data over the WebSocket connection.

Sending Data

To send data to the server, we can use the emit method of the SocketIOClient object. The emit method takes two arguments: the event name and the data to be sent. For example, to send a message to the server, we could do the following:


socket.emit("sendMessage", ["Hello, Server!"])

We can also specify a callback function that will be called when the server responds to the event. The callback function takes an array of data as its argument:


socket.emit("sendMessage", ["Hello, Server!"]) { data, ack in 
// Handle server response 
}

Receiving Data

To receive data from the server, we can use the on() method of the SocketIOClient object. The on() method takes two arguments: the event name and a callback function to be called when the event is received.

For example, to receive a message from the server, we could do the following:


socket.on("receiveMessage") { data, ack in
    if let message = data[0] as? String {
        print("Received message: \(message)")
    }
}

Disconnecting

To disconnect from the WebSocket server, we can use the disconnect method of the SocketIOClient object:


socket.disconnect()

Now that we understand the basics of a socketed connection implementation, let's create a more functional and valuable implementation with a chat client, complete with a server in NodeJS. I'll develop this example in SwiftUI, so if you're still getting used to this technology, you'll be able to return to the example later and find more information.

Building a Chat Client in iOS With WebSockets

First, let's create the structure that will represent the message for the communication. You can create something as simple as the following:


struct Message: Hashable {
    var username: String
    var text: String
    var id: UUID
}

It contains a username to identify the messenger, the message text, and an ID to make every message unique.

Now, we need to create the chat client class that will handle all the necessary behaviors for a socketed connection with our server. This class will have a connect()/disconnect() method, a sendMessage() method, a receiveMessage() method, and a receiveNewUser() method to handle all the events that can occur in the app communication pipeline.


  class ChatClient: NSObject {
    static let shared = ChatClient()

    var manager: SocketManager!
    var socket: SocketIOClient!
    var username: String!

    override init() {
        super.init()

        manager = SocketManager(socketURL: URL(string: "http://localhost:3000")!)
        socket = manager.defaultSocket
    }

    func connect(username: String) {
        self.username = username
        socket.connect(withPayload: ["username": username])
    }

    func disconnect() {
        socket.disconnect()
    }

    func sendMessage(_ message: String) {
        socket.emit("sendMessage", message)
    }
    
    func sendUsername(_ username: String) {
        socket.emit("sendUsername", username)
    }

    func receiveMessage(_ completion: @escaping (String, String, UUID) -> Void) {
        socket.on("receiveMessage") { data, _ in
            if let text = data[2] as? String,
               let id = data[0] as? String,
               let username = data[1] as? String {
                completion(username, text, UUID.init(uuidString: id) ?? UUID())
            }
        }
    }
    
    func receiveNewUser(_ completion: @escaping (String, [String:String]) -> Void) {
        socket.on("receiveNewUser") { data, _ in
            if let username = data[0] as? String,
               let users = data[1] as? [String:String] {
                completion(username, users)
            }
        }
    }
}

This class connects to a Socket.IO server running at http://localhost:3000. It then sends and receives messages using the sendMessage and receiveMessage methods of the ChatClient class.

Finally, we need to create the chat view so the app displays a messenger-like view for the user to interact with and see other people's messages. Here's a simple implementation:


  struct ChatView: View {
    @State private var message: String = ""
    @State private var messages: [Message] = []
    @State private var username: String = ""
    @State private var users: [String:String] = [:]
    @State private var newUser: String = ""
    @State private var showUsernamePrompt: Bool = true
    @State private var isShowingNewUserAlert = false

    var body: some View {
        NavigationView {
            VStack {
                if showUsernamePrompt {
                    HStack {
                        TextField("Enter your username", text: $username)
                            .textFieldStyle(RoundedBorderTextFieldStyle())

                        Button(action: connect) {
                            Text("Connect")
                        }
                    }
                    .padding()
                } else {
                    List {
                        ForEach(messages, id: \.self) { message in
                            HStack {
                                if message.username == username {
                                    Text("Me:")
                                        .font(.subheadline)
                                        .foregroundColor(.blue)
                                } else {
                                    Text("\(message.username):")
                                        .font(.subheadline)
                                        .foregroundColor(.green)
                                }
                                
                                Text(message.text)
                            }
                        }
                    }

                    HStack {
                        TextField("Enter a message", text: $message)
                            .textFieldStyle(RoundedBorderTextFieldStyle())

                        Button(action: sendMessage) {
                            Text("Send")
                        }
                    }
                    .padding()
                }
            }
            .navigationBarTitle("Awesome Chat \(users.count > 0 ? "(\(users.count) connected)" : "")")
            .navigationBarTitleDisplayMode(.inline)
            .onDisappear {
                ChatClient.shared.disconnect()
            }
            .alert("\(newUser) just joined the chat!",
                   isPresented: $isShowingNewUserAlert) {
                Button("OK", role: .cancel) {
                    isShowingNewUserAlert = false
                }
            }
        }
    }

    func connect() {
        ChatClient.shared.connect(username: username)
        ChatClient.shared.receiveMessage { username, text, id in
            self.receiveMessage(username: username, text: text, id: id)
        }
        ChatClient.shared.receiveNewUser { username, users in
            self.receiveNewUser(username: username, users: users)
        }
        showUsernamePrompt = false
    }

    func sendMessage() {
        ChatClient.shared.sendMessage(message)
        message = ""
    }

    func receiveMessage(username: String, text: String, id: UUID) {
        messages.append(Message(username: username, text: text, id: id))
    }
    
    func receiveNewUser(username: String, users: [String:String]) {
        self.users = users
        self.newUser = username
        
        self.isShowingNewUserAlert = self.username != username
    }
}

And that's it!

Or that would be it—if you had a server actually listening on the other side for connections. Let's address that now.

Building a Server for Our Chat App

To create a new NodeJS project, just create a directory and add a file named server.js on it. Open that file and add the following code:


  const express = require('express');
  const app = express();
  const server = require('http').Server(app);
  const io = require('socket.io')(server);
  const { randomUUID } = require('crypto');
  
  const users = new Map();
  
  io.on('connection', (socket) => {
    let username = socket.handshake.auth.username;
  
    console.log('a user connected');
  
    users.set(socket.id, username);
  
    io.emit('receiveNewUser', username, Object.fromEntries(users));
  
    socket.on('sendMessage', (message) => {
      const username = users.get(socket.id);
      io.emit('receiveMessage', randomUUID(), username, message);
    });
  
    socket.on('disconnect', () => {
      console.log('user disconnected');
      users.delete(socket.id);
    });
  });
  
  server.listen(3000, () => {
    console.log('listening on *:3000');
  });

Now, open a terminal window on the directory where the file is and run the following command:


npm install express socket.io

This will create a new NodeJS project and load all the necessary dependencies.

By the way, if you don't have npm installed on your computer, check out how to do so in this article.

Now, to start the server, all you have to do is run this command:


node server.js

That's it. Your server is now running and listening to http://localhost:3000.

You can now run the app, and after you provide a name and click connect, you'll get the following:

Websockets in iOs screenshot 2

When the user enters a message and presses the Send button, the app sends the message and the username to the server. The server broadcasts the message and username to all connected clients, and the client displays the message in the chat history.

You can find the complete code in these two repositories.

Advantages and Disadvantages of Using Sockets in iOS

As you probably know, not all technologies are perfect. Here are some advantages and disadvantages of using sockets on your application.

Advantages

  • Real-time communication: WebSockets allow for bidirectional communication between the client and server, enabling real-time updates and communication.
  • Low latency: WebSockets have lower latency than traditional HTTP communication because they don't require a request-response cycle for each message.
  • Efficient use of resources: WebSockets use a single connection for all communication, whereas traditional HTTP communication requires a separate connection for each request. This can lead to more efficient use of resources, especially for applications with high traffic.

Disadvantages

  • Complexity: WebSockets involve a more complex implementation compared to traditional HTTP communication.
  • Limited browser support: WebSockets are not supported by all browsers, although most modern browsers do support them.
  • Security: WebSockets do not use the same security measures as HTTPS, so it's essential to implement additional security measures to protect against malicious attacks.

Best Practices for Using WebSockets in iOS

There are many ways to implement WebSockets in iOS, but that doesn't mean you should venture into the unknown without any guidelines. Here are some of the best practices when implementing WebSockets in iOS.

  • Use a library: Working with WebSockets can be complex, especially when handling the low-level details of the WebSocket protocol. A library like Socket.IO can simplify the process and provide a convenient interface for working with WebSockets.
  • Secure the connection: WebSockets don't use the same security measures as HTTPS, so it's essential to implement additional security measures to protect against malicious attacks. Using a secure connection (wss://) and implementing authentication and authorization mechanisms are good ways to start.
  • Handle errors and disconnections: It's crucial to handle errors and disconnections gracefully, as they can occur for various reasons, such as network issues or server downtime. Implementing retry logic and providing feedback to the user in case of errors or disconnections is a good idea.
  • Optimize for performance: WebSockets can be used for high-traffic applications, so it's critical to optimize for performance. This could include minimizing the amount of data sent over the WebSocket connection, using efficient data structures, and minimizing the number of connections.
  • Test thoroughly: As with any application, it's vital to thoroughly test your WebSocket implementation to ensure that it's stable and performs well under different conditions. To start, testing with different network conditions, stress testing with high traffic, and simulating errors and disconnections can be a tremendous asset.

Conclusion

WebSockets can be a potent tool for creating real-time iOS applications, but they also introduce complexity and security concerns. WebSockets may not be the best fit for your particular use case; thus, it's crucial to give it significant thought before moving forward.

If you want to ensure that your code is robust and secure, I recommend you give Waldo's comprehensive toolset for UI testing a check. It requires no coding and is very approachable, even for non-developers.

Automated E2E tests for your mobile app

Waldo provides the best-in-class runtime for all your mobile testing needs.
Get true E2E testing in minutes, not months.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.