Введение: Почему WebSocket важен для Swift-разработчика
Современные мобильные и десктопные приложения всё чаще требуют мгновенного обмена данными. Будь то чат, биржевой терминал, онлайн-игра или система push-уведомлений — пользователи ожидают реакции в реальном времени. Протокол WebSocket стал стандартом для таких задач, а в экосистеме Swift (iOS/macOS) его поддержка реализована нативно через URLSessionWebSocketTask.
В этой статье мы разберём полный цикл работы с WebSocket на Swift: от настройки соединения до создания полноценного real-time приложения. Вы узнаете, как избежать типичных ошибок, организовать переподключение и интегрировать WebSocket в архитектуру MVVM.
Основы WebSocket в Swift: URLSessionWebSocketTask
Начиная с iOS 13 и macOS 10.15, Apple предоставляет встроенный класс URLSessionWebSocketTask в составе Foundation. Это избавляет от необходимости подключать сторонние библиотеки (например, Starscream) для базовых сценариев.
Создание WebSocket соединения
Для установки соединения необходимо создать сессию URLSession и задать конфигурацию:
import Foundation
class WebSocketManager {
private var webSocketTask: URLSessionWebSocketTask?
private let session: URLSession
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.waitsForConnectivity = true
session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
func connect() {
guard let url = URL(string: "wss://echo.websocket.org") else { return }
webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
receiveMessage()
}
}
Обратите внимание на waitsForConnectivity — это позволяет автоматически восстанавливать соединение при появлении сети.
Отправка и получение сообщений
WebSocket поддерживает два типа сообщений: текстовые (String) и бинарные (Data). Рассмотрим пример отправки:
func sendMessage(_ text: String) {
let message = URLSessionWebSocketTask.Message.string(text)
webSocketTask?.send(message) { error in
if let error = error {
print("Ошибка отправки: \(error.localizedDescription)")
}
}
}
func sendData(_ data: Data) {
let message = URLSessionWebSocketTask.Message.data(data)
webSocketTask?.send(message) { error in
// обработка ошибки
}
}
Для получения сообщений используется рекурсивный вызов receive:
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(let message):
switch message {
case .string(let text):
print("Получен текст: \(text)")
case .data(let data):
print("Получены данные: \(data.count) байт")
@unknown default:
break
}
// Продолжаем слушать
self?.receiveMessage()
case .failure(let error):
print("Ошибка приёма: \(error)")
}
}
}
Важно: вызов receive обрабатывает только одно сообщение. Для непрерывного получения данных его нужно вызывать повторно внутри замыкания успеха.
Архитектура real-time приложения на Swift
Для промышленного использования WebSocket необходимо продумать архитектуру. Рекомендуется использовать сервисный слой с протоколом и делегатом, чтобы отделить сетевую логику от UI.
Протокол WebSocket сервиса
protocol WebSocketServiceProtocol: AnyObject {
var isConnected: Bool { get }
func connect()
func disconnect()
func send(text: String)
func send(data: Data)
}
protocol WebSocketServiceDelegate: AnyObject {
func webSocketDidConnect()
func webSocketDidDisconnect(error: Error?)
func webSocketDidReceive(text: String)
func webSocketDidReceive(data: Data)
}
Реализация с обработкой ошибок и переподключением
class WebSocketService: NSObject, WebSocketServiceProtocol {
private var webSocketTask: URLSessionWebSocketTask?
private var session: URLSession!
private var reconnectAttempts = 0
private let maxReconnectAttempts = 5
weak var delegate: WebSocketServiceDelegate?
private(set) var isConnected = false
override init() {
super.init()
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
func connect() {
guard let url = URL(string: "wss://your-server.com/ws") else { return }
webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
listen()
}
func disconnect() {
webSocketTask?.cancel(with: .normalClosure, reason: nil)
isConnected = false
}
// ... методы send и listen (аналогично предыдущему примеру)
private func reconnect() {
guard reconnectAttempts < maxReconnectAttempts else {
delegate?.webSocketDidDisconnect(error: nil)
return
}
reconnectAttempts += 1
let delay = Double(reconnectAttempts) * 2.0 // экспоненциальная задержка
DispatchQueue.global().asyncAfter(deadline: .now() + delay) { [weak self] in
self?.connect()
}
}
}
extension WebSocketService: URLSessionWebSocketDelegate {
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
isConnected = true
reconnectAttempts = 0
delegate?.webSocketDidConnect()
}
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
isConnected = false
delegate?.webSocketDidDisconnect(error: nil)
reconnect()
}
}
Практический пример: real-time чат на SwiftUI
Создадим простой чат, используя WebSocket и SwiftUI. ViewModel будет подписываться на обновления через Combine.
ViewModel для чата
import SwiftUI
import Combine
class ChatViewModel: ObservableObject {
@Published var messages: [Message] = []
@Published var inputText = ""
private let webSocketService = WebSocketService()
private var cancellables = Set<AnyCancellable>()
struct Message: Identifiable {
let id = UUID()
let text: String
let isIncoming: Bool
}
init() {
webSocketService.delegate = self
webSocketService.connect()
}
func sendMessage() {
guard !inputText.isEmpty else { return }
let message = Message(text: inputText, isIncoming: false)
messages.append(message)
webSocketService.send(text: inputText)
inputText = ""
}
}
extension ChatViewModel: WebSocketServiceDelegate {
func webSocketDidReceive