Skip to content

ActionCable is a WebSocket server being released with 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. Also web sockets are now separate from the client.

Notifications You must be signed in to change notification settings

mkll/Action-Cable-Swift-websocket

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ActionCableSwift

Action Cable Swift Tweet

SPM Action Cable Swift Cocoa Pods

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.

Also web sockets client are now separate from the client.

Installation

To install, simply:

Swift Package Manager

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")
            ])
    // ...

Cocoa Pods

Add the following line to your Podfile

    pod 'ActionCableSwift'

and you can import ActionCableSwift

    import ActionCableSwift

Usage


Your WebSocketService should to implement the ACWebSocketProtocol protocol.


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

Снимок экрана 2020-08-28 в 14 05 21

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
        }
    }
}

Next step to use ActionCableSwift

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()

Manual Subscribe to a Channel

client.addOnConnected { (headers) in
    try? channel.subscribe()
}

Channel Callbacks

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)

Perform an Action on a Channel

// Send an action
channel.addOnSubscribe { (channel, optionalMessage) in
    try? channel.sendMessage(actionName: "speak", params: ["test": 10101010101])
}

Authorization & Headers

client.headers = [
    "Authorization": "sometoken"
]

Requirements

Any Web Socket Library, e.g.

Websocket-kit

Starscream

Author

Me

License

ActionCableSwift is available under the MIT license. See the LICENSE file for more info.

About

ActionCable is a WebSocket server being released with 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. Also web sockets are now separate from the client.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Swift 93.8%
  • Ruby 6.2%