Etiqueta: Docker

  • Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Implementing Sign in with Apple in a client-server setup is valuable because it addresses a real-world need that many developers face, especially as Apple requires apps offering third-party login to support it. While Apple’s documentation focuses mainly on the iOS side, there’s often a gap in clear explanations for securely validating Apple ID tokens on the backend — a critical step to prevent security vulnerabilities.

    Since Node.js is a widely used backend for mobile apps, providing a practical, end-to-end guide would help a large audience, fill a common knowledge gap, and position you as an expert who understands both mobile and server-side development, making the post highly useful, shareable, and relevant.

    Apple Sign in

    Apple Sign In is a secure authentication service introduced by Apple in 2019, allowing users to log in to apps and websites using their Apple ID. It emphasizes privacy by minimizing data sharing with third parties, offering features like hiding the user’s real email address through a unique, auto-generated proxy email. Available on iOS, macOS, and web platforms, it provides a fast and convenient alternative to traditional social logins like Google or Facebook.

    Advantages of Apple Sign In
    One of the biggest advantages is enhanced privacy—Apple does not track user activity across apps, and the «Hide My Email» feature protects users from spam and data leaks. It also simplifies the login process with Face ID, Touch ID, or device passcodes, reducing password fatigue. Additionally, Apple Sign In is mandatory for apps that offer third-party logins on iOS, ensuring wider adoption and consistent security standards.

    Inconveniences of Apple Sign In
    A major drawback is its limited availability, as it only works on Apple devices, excluding Android and Windows users. Some developers also criticize Apple for forcing its use on iOS apps while restricting competitor login options. Additionally, if a user loses access to their Apple ID, account recovery can be difficult, potentially locking them out of linked services. Despite these issues, Apple Sign In remains a strong choice for privacy-focused users.

    Dockerized Node.JS server side

    Start by setting up a blank Node.js server using Express.js to handle HTTP requests.

    npm init -y

    Server.js code is following:

    const express = require('express');
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    require('dotenv').config();
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Middleware for parsing JSON
    app.use(express.json());
    
    // Client for look up public keys at Apple
    const client = jwksClient({
        jwksUri: 'https://appleid.apple.com/auth/keys'
    });
    
    // Function for getting public key
    function getAppleKey(header, callback) {
        client.getSigningKey(header.kid, function (err, key) {
            if (err) {
                callback(err);
            } else {
                const signingKey = key.getPublicKey();
                callback(null, signingKey);
            }
        });
    }
    
    // Route for authenticate
    app.post('/auth/apple', (req, res) => {
        const { identityToken } = req.body;
    
        if (!identityToken) {
            return res.status(400).json({ error: 'identityToken missing' });
        }
    
        jwt.verify(identityToken, getAppleKey, {
            algorithms: ['RS256']
        }, (err, decoded) => {
            if (err) {
                console.error('Error verifying token:', err);
                return res.status(401).json({ error: 'Invalid token' });
            }
    
            // decoded contains user data
            console.log('Token verified:', decoded);
    
            res.json({
                success: true,
                user: {
                    id: decoded.sub,
                    email: decoded.email,
                    email_verified: decoded.email_verified
                }
            });
        });
    });
    
    app.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}`);
    });
    

    server.js sets up an Express server that listens for authentication requests using Apple’s Sign-In service. It imports necessary modules like express for routing, jsonwebtoken for verifying JSON Web Tokens (JWTs), and jwks-rsa for retrieving Apple’s public keys used to validate tokens. The server is configured to parse incoming JSON payloads and uses environment variables (loaded via dotenv) to optionally define a custom port.

    The core logic resides in the /auth/apple POST route. When a client sends a request to this endpoint with an identityToken in the body (typically issued by Apple after a successful login), the server first checks if the token is present. It then verifies the token using jsonwebtoken.verify(), passing a custom key retrieval function (getAppleKey). This function uses the jwksClient to fetch the appropriate public key from Apple’s JWKS (JSON Web Key Set) endpoint based on the kid (Key ID) found in the token header.

    If the token is valid, the decoded payload—which includes user-specific data like sub (user ID), email, and email_verified—is extracted and returned in the response as JSON. If token verification fails, an error response with HTTP 401 status is sent. This setup allows backend applications to securely validate Apple identity tokens without hardcoding public keys, keeping the authentication mechanism both dynamic and secure.

    Server is dockerized:

    FROM node:20
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["npm", "start"]

    This Dockerfile sets up a Node.js environment using the node:20 base image, creates a working directory at /usr/src/app, copies package.json and package-lock.json (if present) into it, installs dependencies with npm install, copies the rest of the application files, exposes port 3000 for the container, and finally runs the npm start command to launch the application.

    For building the app just type:

    docker build -t apple-signin-server .

    Finally execute the container:

    docker run -p 3000:3000 apple-signin-server

    Server ready for receiving requests…

    Client iOS Apple Sign in app

    After creating a simple iOS app project, go to the target settings and add the ‘Sign in with Apple’ capability. Then, start by creating a blank Node.js server.

    The next step is the client code itself:

    import SwiftUI
    import AuthenticationServices
    
    struct ContentView: View {
        @State private var userID: String?
        @State private var userEmail: String?
        @State private var userName: String?
        
        var body: some View {
            VStack(spacing: 20) {
                if let userID = userID {
                    Text("Welcome 🎉")
                        .font(.title)
                    Text("User ID: \(userID)")
                    if let name = userName {
                        Text("Name: \(name)")
                    }
                    if let email = userEmail {
                        Text("Email: \(email)")
                    }
                } else {
                    SignInWithAppleButton(
                        .signIn,
                        onRequest: { request in
                            request.requestedScopes = [.fullName, .email]
                        },
                        onCompletion: { result in
                            switch result {
                            case .success(let authorization):
                                handleAuthorization(authorization)
                            case .failure(let error):
                                print("Authentication error: \(error.localizedDescription)")
                            }
                        }
                    )
                    .signInWithAppleButtonStyle(.black)
                    .frame(width: 280, height: 50)
                    .cornerRadius(8)
                    .padding()
                }
            }
            .padding()
        }
        
        private func handleAuthorization(_ authorization: ASAuthorization) {
            if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
                userID = appleIDCredential.user
                userEmail = appleIDCredential.email
                if let fullName = appleIDCredential.fullName {
                    userName = [fullName.givenName, fullName.familyName]
                        .compactMap { $0 }
                        .joined(separator: " ")
                }
                
                if let identityToken = appleIDCredential.identityToken,
                   let tokenString = String(data: identityToken, encoding: .utf8) {
                    authenticateWithServer(identityToken: tokenString)
                }
            }
        }
        
        private func authenticateWithServer(identityToken: String) {
            guard let url = URL(string: "http://localhost:3000/auth/apple") else { return }
            
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            
            let body = ["identityToken": identityToken]
            
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
            
            URLSession.shared.dataTask(with: request) { data, response, error in
                if let data = data,
                   let json = try? JSONSerialization.jsonObject(with: data) {
                    print("Server response:", json)
                } else {
                    print("Error communicating with server:", error?.localizedDescription ?? "Unknown error")
                }
            }.resume()
        }
    }
    
    
    #Preview {
        ContentView()
    }
    

    It defines a user interface for an iOS app that integrates Sign in with Apple. The core logic is built into the ContentView struct, which maintains state variables to store the signed-in user’s ID, name, and email. When the view is rendered, it checks whether the user is already signed in (i.e., if userID is not nil). If the user is authenticated, it displays a welcome message along with the retrieved user details. If not, it shows a «Sign in with Apple» button that initiates the authentication process when tapped.

    When the «Sign in with Apple» button is pressed, it triggers a request for the user’s full name and email. The result of this action is handled in the onCompletion closure. If the sign-in is successful, the handleAuthorization method is called. This function extracts the user’s credentials from the ASAuthorizationAppleIDCredential object, including their user ID, email, and full name (if provided). It also extracts the identity token (a JSON Web Token), which is used to authenticate the user on the app’s backend server.

    The authenticateWithServer function handles the server-side communication. It sends a POST request to http://localhost:3000/auth/apple, passing the identityToken in the JSON body. This token can be verified on the backend to ensure the identity is legitimate and secure. The response from the server (or any error encountered) is printed to the console. This architecture supports secure, privacy-preserving user login using Apple’s authentication services, commonly used in modern iOS apps.

    Apple Sign in integration

    Deploy iOS app with Apple Sign-In in a simulator (not on a real device).

    review

    Simply sign in using your personal iCloud credentials. Once Apple Sign-In is successful on the client side, it sends a request and provides the identityToken.

    Even if you uninstall the app from the device, the identityToken remains unchanged. Therefore, it can reliably be used as a user identifier.

    Conclusions

    From a programming perspective, implementing Apple Sign-In in your apps is straightforward and enhances privacy, as users can choose whether to share their email.

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

    References

  • 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