Socket.io client for Swift cannot connect to node.js server

66 Views Asked by At

I am trying to use the latest version of Swift package socket.io-client-swift to connect to my socket.io server built in Node.js, but no connection ever seems to be made, because nothing is logged in the console indicating that a connection was successfully made - no polling, no established handshake.

These are the logs that occur in the app indicating that a connection cannot be established:

LOG SocketIOClient{/}: Adding handler for event: connect
LOG SocketIOClient{/}: Adding handler for event: disconnect
LOG SocketIOClient{/}: Adding handler for event: error
LOG SocketIOClient{/}: Handling event: statusChange with data: [connecting, 2]
LOG SocketIOClient{/}: Joining namespace /
LOG SocketManager: Tried connecting socket when engine isn't open. Connecting
LOG SocketManager: Adding engine
LOG SocketEngine: Starting engine. Server: https://ws.my-socket-server.com
LOG SocketEngine: Handshaking
LOG SocketManager: Manager is being released
LOG SocketIOClient{/}: Adding handler for event: connect
LOG SocketIOClient{/}: Adding handler for event: disconnect
LOG SocketIOClient{/}: Adding handler for event: error
LOG SocketIOClient{/}: Handling event: statusChange with data: [connecting, 2]
LOG SocketIOClient{/}: Joining namespace /
LOG SocketManager: Tried connecting socket when engine isn't open. Connecting
LOG SocketManager: Tried connecting an already active socket

As you can see, it says "Tried connecting socket when engine isn't open" & releases the Manager very early.

Here is my singleton WebSocketManager I am creating to share the socket with all of my views:

import Foundation
import SocketIO

class WebSocketManager: NSObject, ObservableObject, URLSessionWebSocketDelegate {
    static let shared = WebSocketManager()

    @Published var drawingSocket: SocketIOClient?
    private var drawingSocketManager: SocketManager?
    private let drawingSocketURL = URL(string: "https://ws.my-socket-server.com")!

    override private init() {
        super.init()
        setupDrawingSocket()
    }

    private func setupDrawingSocket() {
        drawingSocketManager = SocketManager(socketURL: drawingSocketURL, config: [.log(true), .reconnectAttempts(5), .reconnectWaitMax(5), .forceWebsockets(true)])
        drawingSocket = drawingSocketManager?.defaultSocket

        drawingSocket?.on(clientEvent: .connect) { _, _ in
            print("Drawing socket connected")
        }
        drawingSocket?.on(clientEvent: .disconnect) { data, _ in
            print("Drawing socket disconnected: \(data)")
        }
        drawingSocket?.on(clientEvent: .error) { data, _ in
            print("Drawing socket error: \(data)")
        }
        if drawingSocket?.status != .connected {
            drawingSocket?.connect()
        }
    }

    func sendDrawingMessage(_ action: String, data: Any) {
        guard let drawingSocket = drawingSocket else {
            print("Drawing socket is not available")
            return
        }

        // Check if the socket is connected before sending a message
        if drawingSocket.status == .connected {
            drawingSocket.emit(action, data as! SocketData)
        } else {
            print("Drawing socket is not connected. Cannot send message.")
        }
    }

    var isDrawingSocketConnected: Bool {
        return drawingSocket?.status == .connected
    }
}

Here is the entrypoint to all of my views:

import SwiftUI

@main
struct my_iosApp: App {
    var body: some Scene {
        WindowGroup {
            Home()
                .environmentObject(WebSocketManager.shared)
                .environmentObject(GlobalStateManager.shared)
        }
    }
}

and finally, here is where I am using the socket to emit a message to the server:

import SocketIO
import SwiftUI

struct GameCodeDisplay: View {
    @EnvironmentObject var globalStateManager: GlobalStateManager
    @EnvironmentObject var webSocketManager: WebSocketManager
    let gameCode: String
    var onCancel: () -> Void
    private var drawingSocket: SocketIOClient

    init(gameCode: String, onCancel: @escaping () -> Void) {
        self.gameCode = gameCode
        self.onCancel = onCancel

        let manager = SocketManager(socketURL: URL(string: "https://ws.my-socket-server.com")!, config: [.log(true), .reconnectAttempts(5), .reconnectWaitMax(5), .forceWebsockets(true)])
        self.drawingSocket = manager.defaultSocket
    }

    var body: some View {
        ZStack {
            Color(red: 115.0 / 255.0, green: 5.0 / 255.0, blue: 60.0 / 255.0)
                .edgesIgnoringSafeArea(.all)

            VStack {
                Spacer() // Pushes content to the center

                Text("Your game code is:")
                    .font(.title)
                    .fontWeight(.bold)
                    .foregroundColor(.white)

                Text(gameCode)
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding(.bottom)

                ScrollView {
                    VStack {
                        ForEach(globalStateManager.players, id: \.self) { player in
                            Text(player)
                                .frame(maxWidth: .infinity)
                                .multilineTextAlignment(.center)
                                .foregroundColor(.white)
                        }
                    }
                }
                .frame(maxHeight: 200) // Adjust as needed

                if globalStateManager.players.count >= 5 {
                    Button("Start!") {
                        startGame(drawingSocket: drawingSocket, gameCode: gameCode)
                    }
                    .disabled(globalStateManager.players.count < 5)
                    .padding()
                    .foregroundColor(.green)
                }

                Button("Cancel") {
                    onCancel()
                    leaveGame(webSocketManager: webSocketManager, globalStateManager: globalStateManager)
                }
                .padding()
                .foregroundColor(.yellow)

                Spacer() // Allows the button to be at the bottom
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .padding() // Add padding around the entire VStack
        }.onAppear {
            setupDrawingSocketEvents()
        }
    }

    private func setupDrawingSocketEvents() {
        guard let drawingSocket = webSocketManager.drawingSocket else {
            print("Drawing socket not available")
            return
        }

        drawingSocket.on(clientEvent: .connect) { _, _ in
            print("Drawing socket connected")
            // Additional code for when the socket connects
        }

        drawingSocket.on(clientEvent: .disconnect) { data, _ in
            print("Drawing socket disconnected: \(data)")
            // Additional code for when the socket disconnects
        }

        drawingSocket.on(clientEvent: .error) { data, _ in
            print("Drawing socket error: \(data)")
            // Additional error handling
        }

        // Connect to the socket if not connected
        if !webSocketManager.isDrawingSocketConnected {
            drawingSocket.connect()
        }
    }

    private func startGame(drawingSocket: SocketIOClient, gameCode: String) {
        print("GameCodeDisplay: Starting game with gameCode - \(gameCode)")
        webSocketManager.sendDrawingMessage("joinGame", data: gameCode)
        webSocketManager.sendDrawingMessage("startGame", data: gameCode)
    }

    private func leaveGame(webSocketManager: WebSocketManager, globalStateManager: GlobalStateManager) {
        print("GameCodeDisplay: Leaving game with gameCode - \(gameCode)")
        webSocketManager.sendLeaveGameMessage(gameCode: gameCode, username: globalStateManager.username)
        webSocketManager.disconnect()
        globalStateManager.players.removeAll()
        globalStateManager.setUsername(usernameToSet: "")
    }
}

Here is Node.js server that the socket is running on. I am using version socket.io: "^4.7.2":

const express = require("express");
const { Server } = require("socket.io");
const cors = require("cors");
const http = require("http");

const allowedOrigins = [
  "https://website-1.com",
  "https://website-2.com",
  "http://localhost:3000",
];
const app = express();
app.use(
  cors({
    origin: function (origin, callback) {
      console.log("origin: ", origin);
      if (!origin) return callback(null, true); // Allow requests with no origin (like mobile apps or curl requests)
      if (allowedOrigins.indexOf(origin) === -1) {
        const msg =
          "The CORS policy for this site does not allow access from the specified Origin.";
        return callback(new Error(msg), false);
      }
      return callback(null, true);
    },
  })
);
app.get("/health-check", (req, res) => res.status(200).send("OK"));
const server = http.createServer(app);
const io = new Server(server, {
  transports: ["websocket"],
  cors: {
    origin: function (origin, callback) {
      if (allowedOrigins.indexOf(origin) !== -1) {
        callback(null, true);
      } else {
        callback(new Error("Not allowed by CORS"));
      }
    },
    methods: ["GET", "POST"],
  },
});

const connectedSockets = new Set();

io.on("connection", (socket) => {
  connectedSockets.add(socket);

  socket.on("joinGame", (gameCode, callback) => {
    socket.join(gameCode);
    console.log(`Socket with ID ${socket.id} joined room: ${gameCode}`);
    if (typeof callback === "function") {
      callback(`Joined room: ${gameCode}`);
    }
  });
  socket.on("startGame", (gameCode) => {
    console.log("Emitting gameStarted to room:", gameCode);
    io.to(gameCode).emit("gameStarted");
  });
});

server.listen(3001, (err) => {
  connectedSockets.forEach((socket) => {
    socket.disconnect(true);
  });
  connectedSockets.clear();
  if (err) throw err;
  console.log("> Ready on http://localhost:3001");
});

I have tried a variety of different ways of creating that singleton I mentioned of the WebSocketManager to no avail. At this point, I am open to any other Swift package alternatives to connect to my socket.io server from my SwiftUI iOS app.

0

There are 0 best solutions below