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:
134
native/ios/MatchLiveTv/Data/Cable/ActionCableClient.swift
Normal file
134
native/ios/MatchLiveTv/Data/Cable/ActionCableClient.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user