WebSocket на Swift: реальное время для iOS и macOS

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в WebSocket на Swift: от теории к продвинутой практике

В современной мобильной разработке real-time взаимодействие стало не просто фишкой, а необходимостью. Пользователи ожидают, что чаты, ленты новостей, котировки и уведомления будут обновляться мгновенно, без лишних жестов pull-to-refresh. WebSocket — это тот самый протокол, который позволяет установить постоянное двунаправленное соединение между клиентом (вашим приложением на Swift) и сервером.

В этой статье мы выйдем за рамки простого "Hello, World!" и разберем продвинутые техники работы с WebSocket на Swift для платформ iOS и macOS. Вы узнаете, как настроить надежное соединение, обрабатывать сложные сценарии (потеря связи, переподключение, фоновый режим) и строить архитектуру, которая выдержит нагрузку реального биржевого приложения или чата с тысячами пользователей.

Мы рассмотрим как нативный URLSessionWebSocketTask (iOS 13+), так и популярную библиотеку Starscream для версий ниже. Код будет полностью рабочим и готовым к интеграции.

1. Продвинутая настройка WebSocket клиента на Swift

Начнем с фундамента. Вместо тривиального подключения, создадим менеджер, который будет управлять жизненным циклом соединения, обрабатывать ошибки и поддерживать reconnect (автоматическое переподключение).

1.1. Менеджер с автоматическим переподключением (Reconnect)

Реализуем класс WebSocketManager, который использует URLSessionWebSocketTask. Ключевая особенность — экспоненциальная задержка между попытками переподключения, чтобы не перегружать сервер.

import Foundation

final class WebSocketManager: NSObject {
    private var webSocketTask: URLSessionWebSocketTask?
    private var session: URLSession?
    private var isConnected = false
    private var reconnectAttempt = 0
    private let maxReconnectDelay: TimeInterval = 30 // Максимальная задержка 30 сек
    private let baseDelay: TimeInterval = 2 // Базовая задержка 2 сек
    
    var onMessageReceived: ((String) -> Void)?
    var onConnectionStateChanged: ((Bool) -> Void)?
    
    func connect(url: URL) {
        session = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
        let request = URLRequest(url: url, timeoutInterval: 10)
        webSocketTask = session?.webSocketTask(with: request)
        webSocketTask?.resume()
        receiveMessage()
    }
    
    private func reconnect() {
        guard let url = webSocketTask?.originalRequest?.url else { return }
        let delay = min(baseDelay * pow(2.0, Double(reconnectAttempt)), maxReconnectDelay)
        print("Reconnecting in \(delay) seconds... (Attempt \(reconnectAttempt + 1))")
        DispatchQueue.global().asyncAfter(deadline: .now() + delay) { [weak self] in
            self?.connect(url: url)
            self?.reconnectAttempt += 1
        }
    }
    
    private func receiveMessage() {
        webSocketTask?.receive { [weak self] result in
            switch result {
            case .success(let message):
                switch message {
                case .string(let text):
                    self?.onMessageReceived?(text)
                case .data(let data):
                    if let text = String(data: data, encoding: .utf8) {
                        self?.onMessageReceived?(text)
                    }
                @unknown default:
                    break
                }
                self?.reconnectAttempt = 0 // Сброс счетчика при успешном получении
                self?.receiveMessage() // Продолжаем слушать
            case .failure(let error):
                print("Receive error: \(error.localizedDescription)")
                self?.disconnect()
                self?.reconnect()
            }
        }
    }
    
    func send(text: String) {
        let message = URLSessionWebSocketTask.Message.string(text)
        webSocketTask?.send(message) { error in
            if let error = error {
                print("Send error: \(error.localizedDescription)")
            }
        }
    }
    
    func disconnect() {
        webSocketTask?.cancel(with: .goingAway, reason: nil)
        isConnected = false
        onConnectionStateChanged?(false)
    }
}

extension WebSocketManager: URLSessionWebSocketDelegate {
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
        isConnected = true
        reconnectAttempt = 0
        print("WebSocket connected")
        onConnectionStateChanged?(true)
    }
    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        isConnected = false
        print("WebSocket disconnected with code: \(closeCode.rawValue)")
        onConnectionStateChanged?(false)
        reconnect()
    }
}

Этот менеджер решает главную проблему продакшн-приложений — устойчивость к обрывам связи. Обратите внимание на делегат didCloseWith: он автоматически запускает переподключение.

2. Реализация real-time чата с поддержкой комнат

Теперь построим на базе нашего менеджера простой, но показательный чат. Будем использовать протокол JSON для обмена сообщениями.

2.1. Модели данных и парсинг

Создадим модель сообщения и структуру для отправки/приема.

struct ChatMessage: Codable {
    let type: String // "join", "leave", "message"
    let room: String
    let sender: String
    let text: String?
    let timestamp: TimeInterval
}

// Пример отправки сообщения в комнату
func sendChatMessage(manager: WebSocketManager, room: String, sender: String, text: String) {
    let message = ChatMessage(type: "message", room: room, sender: sender, text: text, timestamp: Date().timeIntervalSince1970)
    if let jsonData = try? JSONEncoder().encode(message),
       let jsonString = String(data: jsonData, encoding: .utf8) {
        manager.send(text: jsonString)
    }
}

2.2. Обработка входящих сообщений в реальном времени

В контроллере чата подключаемся к менеджеру и обновляем UI.

class ChatViewController: UIViewController {
    private let webSocketManager = WebSocketManager()
    @IBOutlet weak var tableView: UITableView!
    private var messages: [ChatMessage] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupWebSocket()
    }
    
    private func setupWebSocket() {
        guard let url = URL(string: "wss://your-server.com/chat") else { return }
        
        webSocketManager.onMessageReceived = { [weak self] text in
            guard let self = self,
                  let data = text.data(using: .utf8),
                  let message = try? JSONDecoder().decode(ChatMessage.self, from: data) else { return }
            
            DispatchQueue.main.async {
                self.messages.append(message)
                self.tableView.reloadData()
                // Прокрутка к последнему сообщению
                let indexPath = IndexPath(row                

Блоги

Рекомендаций по книгам