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