Aggiunge app iOS nativa con parità multi-sport e test unitari.

Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Emiliano Frascaro
2026-06-14 21:37:32 +02:00
parent b798e0ea06
commit 585332e32e
101 changed files with 7910 additions and 5 deletions

View File

@@ -0,0 +1,134 @@
import Foundation
final class ActionCableClient: NSObject, URLSessionWebSocketDelegate, @unchecked Sendable {
protocol Listener: AnyObject {
func onConnected()
func onDisconnected()
func onChannelMessage(_ payload: [String: Any])
}
private let cableUrl: URL
private let accessToken: String
private var webSocket: URLSessionWebSocketTask?
private var channelIdentifier: String?
private weak var listener: Listener?
private let session: URLSession
init(cableUrl: String, accessToken: String) {
self.cableUrl = URL(string: cableUrl)!
self.accessToken = accessToken
let config = URLSessionConfiguration.default
self.session = URLSession(configuration: config)
super.init()
}
func connect(channel: String, params: [String: String], listener: Listener) {
disconnect()
self.listener = listener
var identifier: [String: Any] = ["channel": channel]
params.forEach { identifier[$0.key] = $0.value }
channelIdentifier = jsonString(identifier)
var request = URLRequest(url: cableUrl)
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
webSocket = session.webSocketTask(with: request)
webSocket?.resume()
receiveLoop()
sendSubscribe()
}
func performReceive(payload: [String: Any]) {
guard let identifier = channelIdentifier else { return }
var data = payload
data["action"] = "receive"
let envelope: [String: Any] = [
"command": "message",
"identifier": identifier,
"data": jsonString(data) ?? "{}",
]
send(json: envelope)
}
func disconnect() {
webSocket?.cancel(with: .goingAway, reason: nil)
webSocket = nil
channelIdentifier = nil
listener?.onDisconnected()
}
private func sendSubscribe() {
guard let identifier = channelIdentifier else { return }
send(json: ["command": "subscribe", "identifier": identifier])
}
private func receiveLoop() {
webSocket?.receive { [weak self] result in
guard let self else { return }
switch result {
case .success(let message):
switch message {
case .string(let text):
self.handleIncoming(text)
case .data(let data):
if let text = String(data: data, encoding: .utf8) { self.handleIncoming(text) }
@unknown default:
break
}
self.receiveLoop()
case .failure:
self.listener?.onDisconnected()
}
}
}
private func handleIncoming(_ text: String) {
guard let data = text.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
if let type = json["type"] as? String, type == "ping" {
let pong: [String: Any] = ["type": "pong", "message": json["message"] ?? Date().timeIntervalSince1970]
send(json: pong)
return
}
if json["type"] as? String == "confirm_subscription" {
listener?.onConnected()
return
}
if json["type"] as? String == "disconnect" {
listener?.onDisconnected()
return
}
if let message = json["message"] {
var payload: [String: Any]?
if let dict = message as? [String: Any] {
if let nested = dict["data"] as? [String: Any] {
payload = nested
} else if dict["method"] as? String == "receive_message",
let nested = dict["data"] as? [String: Any] {
payload = nested
} else {
payload = dict
}
} else if let str = message as? String,
let d = str.data(using: .utf8),
let dict = try? JSONSerialization.jsonObject(with: d) as? [String: Any] {
payload = dict
}
if let payload { listener?.onChannelMessage(payload) }
}
}
private func send(json: [String: Any]) {
guard let text = jsonString(json) else { return }
webSocket?.send(.string(text)) { _ in }
}
private func jsonString(_ object: Any) -> String? {
guard let data = try? JSONSerialization.data(withJSONObject: object),
let text = String(data: data, encoding: .utf8) else { return nil }
return text
}
}