Support Action Cable Swift development by giving a ⭐️
Action Cable Swift is a client library being released for Action Cable Rails 5 which makes it easy to add real-time features to your app. This Swift client inspired by "Swift-ActionCableClient", but it not support now and I created Action-Cable-Swift.
To install, simply:
Add the following line to your Package.swift
// ...
.package(name: "ActionCableSwift", url: "https://github.com/nerzh/Action-Cable-Swift.git", from: "0.3.2"),
targets: [
.target(
name: "YourPackageName",
dependencies: [
.product(name: "ActionCableSwift", package: "ActionCableSwift")
])
// ...
Add the following line to your Podfile
pod 'ActionCableSwift'
and you can import ActionCableSwift
import ActionCableSwift
Use with Websocket-kit
I highly recommend not using Starscream to implement a WebSocket, because they have a strange implementation that does not allow conveniently reconnecting to a remote server after disconnecting. There is also a cool and fast alternative from the Swift Server Work Group (SSWG), package named Websocket-kit.
Websocket-kit is SPM(Swift Package Manager) client library built on Swift-NIO
Package.swift
// ...
dependencies: [
.package(name: "ActionCableSwift", url: "https://github.com/nerzh/Action-Cable-Swift.git", from: "0.3.0"),
.package(name: "websocket-kit", url: "https://github.com/vapor/websocket-kit.git", .upToNextMinor(from: "2.0.0"))
],
targets: [
.target(
name: "YourPackageName",
dependencies: [
.product(name: "ActionCableSwift", package: "ActionCableSwift"),
.product(name: "WebSocketKit", package: "websocket-kit")
])
// ...
or inside xcode
SPOILER: Recommended implementation WSS based on Websocket-kit(Swift-NIO)
This is propertyWrapper for threadsafe access to webSocket instance
import Foundation
@propertyWrapper
struct Atomic<Value> {
private var value: Value
private let lock = NSLock()
init(wrappedValue value: Value) {
self.value = value
}
var wrappedValue: Value {
get { return load() }
set { store(newValue: newValue) }
}
func load() -> Value {
lock.lock()
defer { lock.unlock() }
return value
}
mutating func store(newValue: Value) {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
This is implementation WSS
import NIO
import NIOHTTP1
import NIOWebSocket
import WebSocketKit
final class WSS: ACWebSocketProtocol {
var url: URL
private var eventLoopGroup: EventLoopGroup
@Atomic var ws: WebSocket?
init(stringURL: String, coreCount: Int = System.coreCount) {
url = URL(string: stringURL)!
eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: coreCount)
}
var onConnected: ((_ headers: [String : String]?) -> Void)?
var onDisconnected: ((_ reason: String?) -> Void)?
var onCancelled: (() -> Void)?
var onText: ((_ text: String) -> Void)?
var onBinary: ((_ data: Data) -> Void)?
var onPing: (() -> Void)?
var onPong: (() -> Void)?
func connect(headers: [String : String]?) {
var httpHeaders: HTTPHeaders = .init()
headers?.forEach({ (name, value) in
httpHeaders.add(name: name, value: value)
})
let promise: EventLoopPromise<Void> = eventLoopGroup.next().makePromise(of: Void.self)
WebSocket.connect(to: url.absoluteString,
headers: httpHeaders,
on: eventLoopGroup
) { ws in
self.ws = ws
ws.onPing { [weak self] (ws) in
self?.onPing?()
}
ws.onPong { [weak self] (ws) in
self?.onPong?()
}
ws.onClose.whenComplete { [weak self] (result) in
switch result {
case .success:
self?.onDisconnected?(nil)
self?.onCancelled?()
case let .failure(error):
self?.onDisconnected?(error.localizedDescription)
self?.onCancelled?()
}
}
ws.onText { (ws, text) in
self.onText?(text)
}
ws.onBinary { (ws, buffer) in
var data: Data = Data()
data.append(contentsOf: buffer.readableBytesView)
self.onBinary?(data)
}
}.cascade(to: promise)
promise.futureResult.whenSuccess { [weak self] (_) in
guard let self = self else { return }
self.onConnected?(nil)
}
}
func disconnect() {
ws?.close(promise: nil)
}
func send(data: Data) {
ws?.send([UInt8](data))
}
func send(data: Data, _ completion: (() -> Void)?) {
let promise: EventLoopPromise<Void>? = ws?.eventLoop.next().makePromise(of: Void.self)
ws?.send([UInt8](data), promise: promise)
promise?.futureResult.whenComplete { (_) in
completion?()
}
}
func send(text: String) {
ws?.send(text)
}
func send(text: String, _ completion: (() -> Void)?) {
let promise: EventLoopPromise<Void>? = ws?.eventLoop.next().makePromise(of: Void.self)
ws?.send(text, promise: promise)
promise?.futureResult.whenComplete { (_) in
completion?()
}
}
}
Use with Starscream
pod 'Starscream', '~> 4.0.0'
SPOILER: If you still want to use "Starscream", then you can to copy this code for websocket client
import Foundation
import Starscream
class WSS: ACWebSocketProtocol, WebSocketDelegate {
var url: URL
var ws: WebSocket
init(stringURL: String) {
url = URL(string: stringURL)!
ws = WebSocket(request: URLRequest(url: url))
ws.delegate = self
}
var onConnected: ((_ headers: [String : String]?) -> Void)?
var onDisconnected: ((_ reason: String?) -> Void)?
var onCancelled: (() -> Void)?
var onText: ((_ text: String) -> Void)?
var onBinary: ((_ data: Data) -> Void)?
var onPing: (() -> Void)?
var onPong: (() -> Void)?
func connect(headers: [String : String]?) {
ws.request.allHTTPHeaderFields = headers
ws.connect()
}
func disconnect() {
ws.disconnect()
}
func send(data: Data) {
ws.write(data: data)
}
func send(data: Data, _ completion: (() -> Void)?) {
ws.write(data: data, completion: completion)
}
func send(text: String) {
ws.write(string: text)
}
func send(text: String, _ completion: (() -> Void)?) {
ws.write(string: text, completion: completion)
}
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(let headers):
onConnected?(headers)
case .disconnected(let reason, let code):
onDisconnected?(reason)
case .text(let string):
onText?(string)
case .binary(let data):
onBinary?(data)
case .ping(_):
onPing?()
case .pong(_):
onPong?()
case .cancelled:
onCancelled?()
default: break
}
}
}
import ActionCableSwift
/// web socket client
let ws: WSS = .init(stringURL: "ws://localhost:3001/cable")
/// action cable client
let clientOptions: ACClientOptions = .init(debug: false, reconnect: true)
let client: ACClient = .init(ws: ws, options: clientOptions)
/// pass headers to connect
/// on server you can get this with env['HTTP_COOKIE']
client.headers = ["COOKIE": "Value"]
/// make channel
/// buffering - buffering messages if disconnect and flush after reconnect
let channelOptions: ACChannelOptions = .init(buffering: true, autoSubscribe: true)
/// params to subscribe passed inside the identifier dictionary
let identifier: [String: Any] = ["key": "value"]
let channel: ACChannel = client.makeChannel(name: "RoomChannel", identifier: identifier, options: channelOptions)
// !!! Make sure that the client and channel objects is declared "globally" and lives while your socket connection is needed
channel.addOnSubscribe { (channel, optionalMessage) in
print(optionalMessage)
}
channel.addOnMessage { (channel, optionalMessage) in
print(optionalMessage)
}
channel.addOnPing { (channel, optionalMessage) in
print("ping")
}
/// Connect
client.connect()
client.addOnConnected { (headers) in
try? channel.subscribe()
}
func addOnMessage(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
func addOnSubscribe(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
func addOnUnsubscribe(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
func addOnRejectSubscription(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
func addOnPing(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
// Send an action
channel.addOnSubscribe { (channel, optionalMessage) in
try? channel.sendMessage(actionName: "speak", params: ["test": 10101010101])
}
client.headers = [
"Authorization": "sometoken"
]
Any Web Socket Library, e.g.
Me
ActionCableSwift is available under the MIT license. See the LICENSE file for more info.