Etiqueta: javascript

  • WebSockets Made Easy: Create a Simple Chat App in iOS

    WebSockets Made Easy: Create a Simple Chat App in iOS

    In this post, I will highlight how WebSockets enable real-time communication with minimal complexity. By leveraging WebSockets, developers can implement instant message delivery without relying on complex polling or delayed responses. This is crucial for providing a smooth user experience in chat applications. With iOS’s native support for WebSockets—such as URLSessionWebSocketTask—this post will demonstrate a simple, modern, and efficient solution for real-time messaging, while teaching developers essential skills like asynchronous communication and network management.

    In this tutorial, we will create a server using a Dockerized Node.js environment and two client applications: a simple HTML-JavaScript client and an iOS app WebSocket client.

    Websocket chat server

    To avoid confusion, let’s create a server folder to store all the necessary files. The first step is to create a new, blank Node.js project

    npm init -y
    Next setup library dependencies required.
    npm install ws express cors
    The libraries ws, express, and cors are installed on the server-side to provide essential functionalities for a modern web application. The ‘ws’ library enables WebSocket implementation in Node.js, allowing real-time bidirectional communication between clients and the server, which is crucial for chat applications. Express is a web application framework for Node.js that simplifies the creation of HTTP servers and route handling, making it easier to set up and manage the web application. Lastly, the ‘cors’ library is used to enable Cross-Origin Resource Sharing (CORS), a security mechanism that controls access to resources from different domains, ensuring that the server can safely interact with clients from various origins. Together, these libraries create a robust server capable of handling WebSocket connections, efficient HTTP routing, and secure cross-origin resource sharing.
    ‘server.js’ will contain our server code:
    const WebSocket = require('ws');
    const express = require('express');
    const cors = require('cors');
    
    const app = express();
    app.use(cors());
    
    const server = app.listen(8080, () => {
      console.log('Servidor HTTP escuchando en el puerto 8080');
    });
    
    const wss = new WebSocket.Server({ server });
    
    wss.on('connection', (ws) => {
      console.log('Cliente conectado');
    
      ws.on('message', (message) => {
        console.log(`Mensaje recibido: ${message}`);
        
        // Enviar el mensaje a todos los clientes conectados
        wss.clients.forEach((client) => {
          if (client.readyState === WebSocket.OPEN) {
            client.send(message.toString());
          }
        });
      });
    
      ws.on('close', () => {
        console.log('Cliente desconectado');
      });
    });
    
    This code sets up a WebSocket server integrated with an Express HTTP server running on port 8080. It allows real-time communication between the server and connected WebSocket clients. The server uses CORS middleware to handle cross-origin requests. When a client connects to the WebSocket server, a connection event is logged. The server listens for messages from the client, logs received messages, and broadcasts them to all connected clients that have an open WebSocket connection. It also logs when a client disconnects. This code facilitates bidirectional, real-time message distribution among multiple WebSocket clients.
    Lets dockerize the server, create ‘Dockerfile’:
    FROM node:14
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 8080
    CMD ["node", "server.js"]
    This Dockerfile sets up a containerized environment for a Node.js application using the Node.js 14 image. It configures the working directory, copies application files and dependencies, installs the required Node.js packages, exposes port 8080 for the application, and specifies that server.js should run using Node.js when the container starts.
    Now is time to create docker image:
    docker build -t websocket-chat-server .

    Finally run the image:

    docker run -p 8080:8080 -d websocket-chat-server
    Screenshot

    For validating websocket server we will create an small html-javascript client:

    <!DOCTYPE html>
    <html>
    <body>
      <ul id="messages"></ul>
      <input type="text" id="messageInput" placeholder="Write a message">
      <button onclick="sendMessage()">Send</button>
    
      <script>
        const socket = new WebSocket('ws://localhost:8080');
    
        socket.onopen = function(event) {
          console.log('Setup connection', event);
        };
    
        socket.onmessage = function(event) {
          const messages = document.getElementById('messages');
          const li = document.createElement('li');
          li.textContent = event.data;
          messages.appendChild(li);
        };
    
        function sendMessage() {
          const input = document.getElementById('messageInput');
          const message = input.value;
          socket.send(message);
          input.value = '';
        }
      </script>
    </body>
    </html>

    This HTML code creates a basic web-based chat interface using WebSocket for real-time communication. It consists of an unordered list (<ul>) to display messages, an input field (<input>) for entering messages, and a «Send» button. The script establishes a WebSocket connection to a server at ws://localhost:8080. When the connection opens, a log message is displayed in the console. Incoming messages from the WebSocket server are dynamically added as list items (<li>) to the message list. When the «Send» button is clicked, the sendMessage function retrieves the user’s input, sends it to the server via the WebSocket, and clears the input field.

    Open file with your favourite browser:

    Screenshot

    Console log show that is properly connected and messages written are properly broadcasted

    websocket iOS Client

    We will follow the same design as we did with HTML and JavaScript:

    struct ContentView: View {
        @StateObject private var webSocketManager = WebSocketManager()
        @State private var messageText = ""
        
        var body: some View {
            VStack {
                List(webSocketManager.messages, id: \.self) { message in
                    Text(message)
                }
                
                HStack {
                    TextField("Enter message", text: $messageText)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                    
                    Button("Send") {
                        webSocketManager.send(messageText)
                        messageText = ""
                    }
                }.padding()
            }
            .onAppear {
                webSocketManager.connect()
            }
        }
    }
    Code defines a ContentView that interacts with a WebSocket connection to display and send messages in a real-time chat interface. It uses a WebSocketManager (assumed to handle WebSocket connections and messaging) as a @StateObject, ensuring it persists across view updates. The body consists of a VStack with a List that dynamically displays messages received via the WebSocket, and an input section with a TextField for composing messages and a Button to send them. When the button is pressed, the typed message is sent via the webSocketManager, and the input field is cleared. The onAppear modifier ensures that the WebSocket connection is initiated when the view appears on screen.
    Finally WebSocketManager is where all magic takes place:
    class WebSocketManager: ObservableObject {
        private var webSocketTask: URLSessionWebSocketTask?
        @Published var messages: [String] = []
        
        func connect() {
            let url = URL(string: "ws://localhost:8080")!
            webSocketTask = URLSession.shared.webSocketTask(with: url)
            webSocketTask?.resume()
            receiveMessage()
        }
        
        func send(_ message: String) {
            webSocketTask?.send(.string(message)) { error in
                if let error = error {
                    print("Error sending message: \(error)")
                }
            }
        }
        
        private func receiveMessage() {
            webSocketTask?.receive { result in
                switch result {
                case .failure(let error):
                    print("Error receiving message: \(error)")
                case .success(let message):
                    switch message {
                    case .string(let text):
                        DispatchQueue.main.async {
                            self.messages.append(text)
                        }
                    default:
                        break
                    }
                    self.receiveMessage()
                }
            }
        }
    }
    The WebSocketManager class manages a WebSocket connection and handles sending and receiving messages. It uses URLSessionWebSocketTask to connect to a WebSocket server at a specified URL (ws://localhost:8080) and maintains an observable array of received messages, messages, for use in SwiftUI or other reactive contexts. The connect method establishes the connection and starts listening for incoming messages using the private receiveMessage method, which recursively listens for new messages and appends them to the messages array on the main thread. The send method allows sending a string message over the WebSocket, with error handling for failures. This class encapsulates WebSocket communication in a way that supports reactive UI updates.
    Finally, place both front ends (iPhone and web client) side by side. If you followed the instructions, you should have a chat between them.

    Conclusions

    WebSocket is a server technology, distinct from REST APIs or GraphQL, that is particularly well-suited for real-time, bidirectional communication. It’s ideal for applications that require fast, continuous interactions, such as real-time chats, online games, and collaborative tools (e.g., Figma, Google Docs). I hope you enjoyed reading this as much as I enjoyed writing and programming it.

    You can find the source code used for this post in the repository linked below.

    References

  • Crafting a Simple iOS App Using GraphQL APIs

    Crafting a Simple iOS App Using GraphQL APIs

    Using GraphQL instead of REST offers greater flexibility and efficiency. It allows clients to request precisely the data they need through a single endpoint, avoiding issues like over-fetching or under-fetching. Its strongly-typed schema enhances the developer experience by providing built-in documentation and easy introspection. Additionally, GraphQL’s real-time capabilities, enabled through subscriptions, support features such as live updates. It also excels at aggregating data from multiple sources into a unified API, making it an excellent choice for complex systems. However, it can introduce added server-side complexity and may not be necessary for simple or static applications where REST is sufficient.

    In this post, we will create a minimal, dockerized GraphQL server and implement an iOS client app that performs a request. At the end of the post, you will find a link to a GitHub repository containing the source code for further review.

    Setup a graphQL Server

    In this section, we will develop a minimal GraphQL dockerized server. The purpose of this post is not to dive deeply into GraphQL or Docker. However, I recommend spending some time exploring tutorials on these topics. At the end of the post, you will find links to the tutorials I followed.

    The server code fetches data from hardcoded sources for simplicity. In a typical scenario, the data would be retrieved from a database or other data source:

    import { ApolloServer, gql } from 'apollo-server';
    
    // Sample data
    const users = [
        { id: '1', name: 'Brandon Flowers', email: 'brandon.flowers@example.com' },
        { id: '2', name: 'Dave Keuning', email: 'dave.keuning@example.com' },
        { id: '3', name: 'Ronnie Vannucci Jr.', email: 'ronnie.vannuccijr@example.com' },
        { id: '4', name: 'Mark Stoermer', email: 'mark.stoermer@example.com' },
      ];
    
    // Schema
    const typeDefs = gql`
      type Query {
        getUser(id: ID!): User
      }
    
      type User {
        id: ID!
        name: String!
        email: String!
      }
    `;
    
    // Resolver
    const resolvers = {
      Query: {
        getUser: (_, { id }) => {
          const user =  users.find(user => user.id === id);
          if (!user) {
            throw new Error(`User with ID ${id} not found`);
          }
          return user;
        },
      },
    };
    
    // Setup server
    const server = new ApolloServer({ typeDefs, resolvers });
    
    // Start up server
    server.listen().then(({ url }) => {
      console.log(`🚀 Servidor listo en ${url}`);
    });
    The server is containerized using Docker, eliminating the need to install npm on your local machine. It will be deployed within a Linux-based image preconfigured with Node.js:
    # Usamos una imagen oficial de Node.js
    FROM node:18
    
    # Establecemos el directorio de trabajo
    WORKDIR /usr/src/app
    
    # Copiamos los archivos del proyecto a la imagen
    COPY . .
    
    # Instalamos las dependencias del proyecto
    RUN npm install
    
    # Exponemos el puerto 4000
    EXPOSE 4000
    
    # Ejecutamos el servidor
    CMD ["node", "server.js"]

    This Dockerfile packages a Node.js application into a container. When the container is run, it performs the following actions:

    1. Sets up the application directory.
    2. Installs the required dependencies.
    3. Starts the Node.js server located in server.js, making the application accessible on port 4000.

    To build the Docker image, use the following command:

    docker build -t graphql-server .
    Once the image is built, simply run the container image
    docker run -p 4000:4000 graphql-server
    Type ‘http://localhost:4000/’ URL on your favourite browser:
    The GraphQL server is now online. To start querying the server, simply click ‘Query your server,’ and the Sandbox will open for you to begin querying. The sample query that we will execute is as follows:
    query  {
      getUser(id: "4") {
        id
        name
        email
      }
    }
    Up to this point, the server is ready to handle requests. In the next section, we will develop an iOS sample app client.

    Sample iOS graphQL client app

    For the sample iOS GraphQL client app, we will follow the MVVM architecture. The app will use Swift 6 and have Strict Concurrency Checking enabled. The app’s usage is as follows:

    The user enters an ID (from 1 to 4), and the app prompts for the user’s name. The server then responds with the name associated with that ID. I will skip the view and view model components, as there is nothing new to discuss there. However, if you’re interested, you can find a link to the GitHub repository.

    The key aspect of the implementation lies in the GraphQLManager, which is responsible for fetching GraphQL data. Instead of using a GraphQL SPM component like Apollo-iOS, I chose to implement the data fetching using URLSession. This decision was made to avoid introducing a third-party dependency. At this level, the code remains simple, and I will not expand further on this in the post.

    Regarding Swift 6 compliance, the code is executed within a @GlobalActor to avoid overloading the @MainActor.

    import SwiftUI
    import Foundation
    
    @globalActor
    actor GlobalManager {
        static var shared = GlobalManager()
    }
    
    @GlobalManager
    protocol GraphQLManagerProtocol {
        func fetchData(userId: String) async -> (Result<User, Error>)
    }
    
    @GlobalManager
    class GraphQLManager: ObservableObject {
    
        @MainActor
        static let shared = GraphQLManager()
    
    }
    
    extension GraphQLManager: GraphQLManagerProtocol {
    
        func fetchData(userId: String) async -> (Result<User, Error>) {
            
            let url = URL(string: "http://localhost:4000/")!
            let query = """
            query  {
              getUser(id: "\(userId)") {
                id
                name
              }
            }
            """
            
            let body: [String: Any] = [
                "query": query
            ]
            guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else {
                return .failure(NSError(domain: "Invalid JSON", code: 400, userInfo: nil))
            }
            
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = jsonData
            
            do {
                let (data, response) = try await URLSession.shared.data(for: request)
                guard let httpResponse = response as? HTTPURLResponse,
                    (200...299).contains(httpResponse.statusCode) else {
                    return .failure(ErrorService.invalidHTTPResponse)
                }
                do {
                    let graphQLResponse = try JSONDecoder().decode(GraphQLResponse<GraphQLQuery>.self, from: data)
                    return .success(graphQLResponse.data.user)
                } catch {
                    return .failure(ErrorService.failedOnParsingJSON)
                }
            } catch {
                return .failure(ErrorService.errorResponse(error))
            }
        }
      
    }

    Conclusions

    GraphQL is another alternative for implementing client-server requests. It does not differ significantly from the REST approach. You can find source code used for writing this post in following repository.

    References

  • Bridging  Data Transfer from WKWebView to iOS

    Bridging Data Transfer from WKWebView to iOS

    The aim of this post is to bridge the gap between web technologies and native iOS development by enabling data transfer from the web side to the app. In some native apps, it is common to have a WebView control rendering web content, and it is not unusual for the app to require data from the web content for further tasks.

    In this post, we simulate a local web server using a Docker container running an HTML+JavaScript page that displays a button. When the button is pressed, a message is sent and captured by the app.

    Web content and web server

    Web content is basically this HTML+JavaScript code:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Communication with SwiftUI</title>
    </head>
    <body>
        <h1>Hello world!</h1>
        <p>This is a HTML page served from a Docker container with Nginx.</p>
        <button id="sendDataBtn">Send Data to App</button>
    
        <script>
            document.getElementById("sendDataBtn").addEventListener("click", function() {
                var data = "Hello from JavaScript!";
                // Send data to the native app
                window.webkit.messageHandlers.callbackHandler.postMessage(data);
            });
        </script>
    </body>
    </html>

     When the button is pressed, a message such as «Hello from JavaScript!» or any custom text of your choice is sent to the app.

    To serve this page, I have chosen Docker. Docker is an open-source platform that allows developers to automate the deployment, scaling, and management of applications within lightweight, portable containers. Containers encapsulate an application and its dependencies, ensuring consistent behavior across different environments, from development to production.

    By providing an isolated and reproducible environment, Docker resolves the classic «it works on my machine» issue. It enhances development efficiency, simplifies deployment processes, and streamlines testing, making it easier to scale and maintain applications across various systems or cloud infrastructures.

    Docker is a fascinating topic! If you’re unfamiliar with it, I highly recommend exploring some tutorials. At the end of this post, I’ve included a list of helpful references.

    Below is the Dockerfile we created to build the Docker image:

    # Nginx base image
    FROM nginx:alpine
    
    # Copy HTML file into container
    COPY index.html /usr/share/nginx/html/index.html
    
    # Expose port 80 (defect port for Nginx)
    EXPOSE 80
    

    This Dockerfile creates a Docker image that serves an HTML file using Nginx on a lightweight Alpine Linux base. Here’s a breakdown of each line:

    1. FROM nginx:alpine:
      This line specifies the base image to use for the Docker container. It uses the official nginx image with the alpine variant, which is a minimal version of Nginx built on the Alpine Linux distribution. This results in a small and efficient image for running Nginx.

    2. COPY index.html /usr/share/nginx/html/index.html:
      This line copies the index.html file from your local directory (where the Dockerfile is located) into the container’s filesystem. Specifically, it places the index.html file into the directory where Nginx serves its static files (/usr/share/nginx/html/). This file will be accessible when the container runs, and Nginx will serve it as the default webpage.

    3. EXPOSE 80:
      This instruction tells Docker that the container will listen on port 80, which is the default port for HTTP traffic. It doesn’t actually publish the port but serves as documentation for which port the container expects to use when run. This is helpful for networking and linking with other containers or exposing the container’s services to the host machine.

    To create the Docker image, open a terminal window in the directory containing the Dockerfile and run:

    $ docker build -t web-server .

    The command docker build -t web-server . builds a Docker image from the Dockerfile in the current directory (.). The resulting image is tagged with the name web-server.

    The web content has been embedded within the image. Therefore, if you modify the content, you will need to recreate the image.

    The next step is to run the container. In the context of programming, a container can be likened to creating an instance of an object.
    $ docker run -d -p 8080:80 web-server

    The command runs a Docker container in detached mode (-d) using the image web-server. It maps port 8080 on the host machine to port 80 inside the container (-p 8080:80)

    The container is now running. Open your favorite web browser and navigate to the following URL: ‘http://localhost:8080‘. The web content should load and be displayed.

    The iOS app

    iOS App basically presents a WebView controller:

    struct ContentView: View {
        @State private var messageFromJS: String = ""
        @State private var showAlert = false
        
        var body: some View {
            VStack {
                
                WebView(url: URL(string: "http://localhost:8080/")!) { message in
                    messageFromJS = message
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
            .onChange(of: messageFromJS) {
                showAlert.toggle()
            }
            .alert(isPresented: $showAlert) {
                Alert(
                    title: Text("Message from JavaScript:"),
                    message: Text("\(messageFromJS)"),
                    dismissButton: .default(Text("OK"))
                )
            }
        }
    }

    If we take a look at WebView:

    import SwiftUI
    import WebKit
    
    struct WebView: UIViewRepresentable {
        var url: URL
        var onMessageReceived: (String) -> Void // Closure to handle messages from JS
    
        class Coordinator: NSObject, WKScriptMessageHandler {
            var parent: WebView
    
            init(parent: WebView) {
                self.parent = parent
            }
    
            // This method is called when JS sends a message to native code
            func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
                if message.name == "callbackHandler" {
                    if let messageBody = message.body as? String {
                        parent.onMessageReceived(messageBody)
                    }
                }
            }
        }
    
        func makeCoordinator() -> Coordinator {
            return Coordinator(parent: self)
        }
    
        func makeUIView(context: Context) -> WKWebView {
            let configuration = WKWebViewConfiguration()
            configuration.userContentController.add(context.coordinator, name: "callbackHandler")
    
            let webView = WKWebView(frame: .zero, configuration: configuration)
            webView.load(URLRequest(url: url))
            return webView
        }
    
        func updateUIView(_ uiView: WKWebView, context: Context) {
            // No need to update the WebView in this case
        }
    }

    The code defines a WebView struct that integrates a WKWebView (a web view) into a SwiftUI interface. The WebView struct conforms to the UIViewRepresentable protocol, allowing it to present UIKit components within SwiftUI. A custom coordinator class (Coordinator) is set up to handle messages sent from JavaScript running inside the web view. Specifically, when JavaScript sends a message using the name "callbackHandler", the userContentController(_:didReceive:) method is triggered. This method passes the message to a closure (onMessageReceived) provided by the WebView, enabling custom handling of the message.

    The makeUIView method creates and configures the WKWebView, including loading a specified URL to display the desired web content. When the project is deployed in a simulator, the web content is rendered properly, demonstrating the effectiveness of this integration.

    This implementation provides a powerful way to integrate web content into a SwiftUI application, enabling dynamic interaction between SwiftUI and JavaScript.

    When we press the ‘Send Data to App’ button:

    Is presented an alert with the message sent from web content.

    Conclusions

    In this post, I have preseted a way to pass information from web to iOS App. In this app we have transfered non-sensitive information because passing information from web content in a WebView to an iOS app poses several security risks, including Cross-Site Scripting (XSS) attacks, data leakage, injection attacks, unauthorized file system access, URL scheme abuse, and mixed content issues. These vulnerabilities can lead to unauthorized access to user data, compromise of app integrity, and exposure of sensitive information. To mitigate these risks, developers should use WKWebView, implement input sanitization, enforce content security policies, use HTTPS, disable unnecessary JavaScript execution, and properly configure WebView restrictions. By adhering to these security practices, developers can significantly reduce the attack surface and enhance the overall security of their iOS applications.

    You can find source code used for writing this post in following repository.

    References