From ee17456dcd5c37455739986b73294ac559dc5acc Mon Sep 17 00:00:00 2001 From: cheykrym Date: Tue, 21 Oct 2025 02:39:48 +0300 Subject: [PATCH] add socket.io --- index2.html | 159 ++++++++++++++++++ yobble.xcodeproj/project.pbxproj | 28 +++ .../xcshareddata/swiftpm/Package.resolved | 24 +++ yobble/Network/AuthNotifications.swift | 5 + yobble/Network/AuthService.swift | 5 + yobble/Network/NetworkClient.swift | 2 + yobble/Services/SocketService.swift | 135 +++++++++++++++ yobble/ViewModels/LoginViewModel.swift | 10 ++ yobble/config.swift | 1 + 9 files changed, 369 insertions(+) create mode 100644 index2.html create mode 100644 yobble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 yobble/Network/AuthNotifications.swift create mode 100644 yobble/Services/SocketService.swift diff --git a/index2.html b/index2.html new file mode 100644 index 0000000..b42f104 --- /dev/null +++ b/index2.html @@ -0,0 +1,159 @@ + + + + Socket.IO Test Client + + + +

Socket.IO Test Client

+ +
+

Подключение

+ + + + +
+ +
+

Отправка клиентского сообщения

+ + +
+ +
+

Настройки

+ +
+ +

Логи

+
+ + + + + diff --git a/yobble.xcodeproj/project.pbxproj b/yobble.xcodeproj/project.pbxproj index a3e6849..9f2db6b 100644 --- a/yobble.xcodeproj/project.pbxproj +++ b/yobble.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 1A85C6CC2EA6FD73009FA847 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = 1A85C6CB2EA6FD73009FA847 /* SocketIO */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ 1A6D61DB2E7CD04000B9F736 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -52,6 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1A85C6CC2EA6FD73009FA847 /* SocketIO in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -112,6 +117,7 @@ ); name = yobble; packageProductDependencies = ( + 1A85C6CB2EA6FD73009FA847 /* SocketIO */, ); productName = yobble; productReference = 1A6D61CC2E7CD03E00B9F736 /* yobble.app */; @@ -196,6 +202,9 @@ ); mainGroup = 1A6D61C32E7CD03E00B9F736; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 1A85C6CA2EA6FC08009FA847 /* XCRemoteSwiftPackageReference "socket" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 1A6D61CD2E7CD03E00B9F736 /* Products */; projectDirPath = ""; @@ -598,6 +607,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 1A85C6CA2EA6FC08009FA847 /* XCRemoteSwiftPackageReference "socket" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/socketio/socket.io-client-swift"; + requirement = { + kind = exactVersion; + version = 16.1.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1A85C6CB2EA6FD73009FA847 /* SocketIO */ = { + isa = XCSwiftPackageProductDependency; + package = 1A85C6CA2EA6FC08009FA847 /* XCRemoteSwiftPackageReference "socket" */; + productName = SocketIO; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 1A6D61C42E7CD03E00B9F736 /* Project object */; } diff --git a/yobble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/yobble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..c865547 --- /dev/null +++ b/yobble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "c9fb241c5f575df8f20b39649006995779013948e60c51c3f85b729f83b054e7", + "pins" : [ + { + "identity" : "socket.io-client-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/socketio/socket.io-client-swift", + "state" : { + "revision" : "42da871d9369f290d6ec4930636c40672143905b", + "version" : "16.1.1" + } + }, + { + "identity" : "starscream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/daltoniam/Starscream", + "state" : { + "revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a", + "version" : "4.0.8" + } + } + ], + "version" : 3 +} diff --git a/yobble/Network/AuthNotifications.swift b/yobble/Network/AuthNotifications.swift new file mode 100644 index 0000000..a4867e0 --- /dev/null +++ b/yobble/Network/AuthNotifications.swift @@ -0,0 +1,5 @@ +import Foundation + +extension Notification.Name { + static let accessTokenDidChange = Notification.Name("accessTokenDidChange") +} diff --git a/yobble/Network/AuthService.swift b/yobble/Network/AuthService.swift index 47aeab3..7e0e590 100644 --- a/yobble/Network/AuthService.swift +++ b/yobble/Network/AuthService.swift @@ -69,6 +69,8 @@ final class AuthService { } UserDefaults.standard.set(username, forKey: "currentUser") + NotificationCenter.default.post(name: .accessTokenDidChange, object: nil) + completion(true, nil) } catch { completion(false, NSLocalizedString("Не удалось обработать ответ сервера.", comment: "")) @@ -175,6 +177,8 @@ final class AuthService { UserDefaults.standard.removeObject(forKey: "currentUser") + NotificationCenter.default.post(name: .accessTokenDidChange, object: nil) + let allUsers = KeychainService.shared.getAllServices() for user in allUsers { let hasAccessToken = KeychainService.shared.get(forKey: "access_token", service: user) != nil @@ -183,6 +187,7 @@ final class AuthService { if hasAccessToken && hasRefreshToken { UserDefaults.standard.set(user, forKey: "currentUser") if AppConfig.DEBUG { print("Logout: переключились на пользователя \(user)") } + NotificationCenter.default.post(name: .accessTokenDidChange, object: nil) completion(true, nil) return } diff --git a/yobble/Network/NetworkClient.swift b/yobble/Network/NetworkClient.swift index 185b2ae..c17a50e 100644 --- a/yobble/Network/NetworkClient.swift +++ b/yobble/Network/NetworkClient.swift @@ -266,6 +266,8 @@ final class NetworkClient { KeychainService.shared.save(userId, forKey: "userId", service: tokenInfo.login) } + NotificationCenter.default.post(name: .accessTokenDidChange, object: nil) + self.completeRefresh(success: true) } catch { self.completeRefresh(success: false) diff --git a/yobble/Services/SocketService.swift b/yobble/Services/SocketService.swift new file mode 100644 index 0000000..4bbddf0 --- /dev/null +++ b/yobble/Services/SocketService.swift @@ -0,0 +1,135 @@ +import Foundation +#if canImport(SocketIO) +import SocketIO +#endif + +final class SocketService { + static let shared = SocketService() + + private let syncQueue = DispatchQueue(label: "org.yobble.socket.service") + private var currentToken: String? + private var currentAuthPayload: [String: Any] = [:] + + #if canImport(SocketIO) + private var manager: SocketManager? + private var socket: SocketIOClient? + #endif + + private init() {} + + func connectForCurrentUser() { + syncQueue.async { [weak self] in + guard let self else { return } + guard let token = self.resolveCurrentAccessToken() else { + if AppConfig.DEBUG { print("[SocketService] No access token available, disconnecting") } + self.currentToken = nil + self.disconnectInternal() + return + } + + self.connectInternal(with: token) + } + } + + func connect(withToken token: String) { + syncQueue.async { [weak self] in + self?.connectInternal(with: token) + } + } + + func disconnect() { + syncQueue.async { [weak self] in + guard let self else { return } + self.currentToken = nil + self.disconnectInternal() + } + } + + private func resolveCurrentAccessToken() -> String? { + guard + let login = UserDefaults.standard.string(forKey: "currentUser"), + !login.isEmpty + else { + return nil + } + + return KeychainService.shared.get(forKey: "access_token", service: login) + } + + private func connectInternal(with token: String) { + #if canImport(SocketIO) + if token == currentToken, + let socket, + socket.status == .connected || socket.status == .connecting { + if AppConfig.DEBUG { print("[SocketService] Already connected with current token") } + return + } + + currentToken = token + currentAuthPayload = ["token": token] + setupSocket(with: token) + socket?.connect(withPayload: currentAuthPayload) + #else + if AppConfig.DEBUG { + print("[SocketService] SocketIO framework not available; skipping connection") + } + #endif + } + + #if canImport(SocketIO) + private func setupSocket(with token: String) { + guard let baseURL = URL(string: AppConfig.API_SERVER) else { + if AppConfig.DEBUG { print("[SocketService] Invalid socket URL: \(AppConfig.API_SERVER)") } + return + } + + disconnectInternal() + + let configuration: SocketIOClientConfiguration = [ + .log(AppConfig.DEBUG), + .compress, + .secure(AppConfig.PROTOCOL.lowercased() == "https"), + .path(AppConfig.SOCKET_PATH), + .reconnects(true), + .reconnectWait(2), + .forceWebsockets(true), + .extraHeaders(["Authorization": "Bearer \(token)"]), + .connectParams(["token": token]) + ] + + let manager = SocketManager(socketURL: baseURL, config: configuration) + manager.handleQueue = syncQueue + let socket = manager.defaultSocket + + if AppConfig.DEBUG { + socket.onAny { event in + print("[SocketService] onAny event=\(event.event) data=\(event.items ?? [])") + } + } + + socket.on(clientEvent: .connect) { _, _ in + if AppConfig.DEBUG { print("[SocketService] Connected") } + } + + socket.on(clientEvent: .disconnect) { data, _ in + if AppConfig.DEBUG { print("[SocketService] Disconnected: \(data)") } + } + + socket.on(clientEvent: .error) { data, _ in + if AppConfig.DEBUG { print("[SocketService] Error: \(data)") } + } + + self.manager = manager + self.socket = socket + } + + private func disconnectInternal() { + socket?.disconnect() + manager?.disconnect() + socket = nil + manager = nil + } + #else + private func disconnectInternal() { } + #endif +} diff --git a/yobble/ViewModels/LoginViewModel.swift b/yobble/ViewModels/LoginViewModel.swift index 8b07628..9f73a08 100644 --- a/yobble/ViewModels/LoginViewModel.swift +++ b/yobble/ViewModels/LoginViewModel.swift @@ -18,6 +18,7 @@ class LoginViewModel: ObservableObject { @Published var isLoggedIn: Bool = false private let authService = AuthService() + private let socketService = SocketService.shared private enum DefaultsKeys { static let currentUser = "currentUser" @@ -38,10 +39,12 @@ class LoginViewModel: ObservableObject { if success { self?.loadStoredUser() self?.isLoggedIn = true + self?.socketService.connectForCurrentUser() } else { self?.isLoggedIn = false self?.errorMessage = error ?? NSLocalizedString("Произошла ошибка.", comment: "") self?.showError = false + self?.socketService.disconnect() } self?.isLoading = false } @@ -59,9 +62,11 @@ class LoginViewModel: ObservableObject { if success { self?.loadStoredUser() self?.isLoggedIn = true + self?.socketService.connectForCurrentUser() } else { self?.errorMessage = error ?? NSLocalizedString("Неизвестная ошибка", comment: "") self?.showError = true + self?.socketService.disconnect() } } } @@ -73,6 +78,9 @@ class LoginViewModel: ObservableObject { if success { self?.isLoggedIn = true // 👈 переключаем на главный экран после автологина self?.loadStoredUser() + self?.socketService.connectForCurrentUser() + } else { + self?.socketService.disconnect() } completion(success, message) } @@ -87,6 +95,7 @@ class LoginViewModel: ObservableObject { self?.password = "" self?.isLoggedIn = true self?.showError = false + self?.socketService.connectForCurrentUser() } else { self?.username = "" self?.userId = "" @@ -94,6 +103,7 @@ class LoginViewModel: ObservableObject { self?.isLoggedIn = false self?.errorMessage = error ?? NSLocalizedString("Ошибка при деавторизации.", comment: "") self?.showError = false + self?.socketService.disconnect() } } } diff --git a/yobble/config.swift b/yobble/config.swift index 5cf05d8..7d55cac 100644 --- a/yobble/config.swift +++ b/yobble/config.swift @@ -5,6 +5,7 @@ struct AppConfig { //static let SERVICE = Bundle.main.bundleIdentifier ?? "default.service" static let PROTOCOL = "https" static let API_SERVER = "\(PROTOCOL)://api.yobble.org" + static let SOCKET_PATH = "/socket.io/" static let USER_AGENT = "yobble ios" static let APP_BUILD = "appstore" // appstore / freestore