ACActionCable is a Swift 5 client for Ruby on Rails 6's Action Cable WebSocket server. It is a hard fork of Action-Cable-Swift. It aims to be well-tested, dependency-free, and easy to use.
If your project doesn't use CocoaPods yet, follow this guide.
Add the following line to your Podfile:
pod'ACActionCable','~> 2'ACActionCable uses semantic versioning.
You can use ACActionCable with any WebSocket library you'd like. Just create a class that implements ACWebSocketProtocol. If you use Starscream, you can just copy ACStarscreamWebSocket into your project.
Create a singleton class to hold an ACClient
// MyClient.swift
import ACActionCable
classMyClient{staticletshared=MyClient()privateletclient:ACClientprivateinit(){letsocket=ACStarscreamWebSocket(stringURL:"https://myrailsapp.com/cable") // Your concrete implementation of ACWebSocketProtocol (see above)
client =ACClient(socket: socket, connectionMonitorTimeout:6)}}If you set a connectionMonitorTimeout and no ping is received for that many seconds, then ACConnectionMonitor will periodically attempt to reconnect. Leave connectionMonitorTimeout nil to disable connection monitoring.
You can set custom headers based on your server's requirements
// MyClient.swift
func connect(){
client.headers =["Auth":"Token","Origin":"https://myrailsapp.com",]
client.connect()}func disconnect(){
client.disconnect()}You probably want to connect when the user's session begins and disconnect when the user logs out.
// User.swift
func onSessionCreated(){MyClient.shared.connect()
// ...
}func logOut(){
// ...
MyClient.shared.disconnect()}// MyClient.swift
func subscribe(to channelIdentifier:ACChannelIdentifier, with messageHandler:@escapingACMessageHandler)->ACSubscription?{guardlet subscription = client.subscribe(to: channelIdentifier, with: messageHandler)else{print("Warning: MyClient ignored attempt to double subscribe. You are already subscribed to \(channelIdentifier)")returnnil}return subscription
}func unsubscribe(from subscription:ACSubscription){
client.unsubscribe(from: subscription)}// ChatChannel.swift
import ACActionCable
classChatChannel{privatevarsubscription:ACSubscription?func subscribe(to roomId:Int){guard subscription ==nilelse{return}letchannelIdentifier=ACChannelIdentifier(channelName:"ChatChannel", identifier:["room_id": roomId])!
subscription =MyClient.shared.subscribe(to: channelIdentifier, with:handleMessage(_:))}func unsubscribe(){guardlet subscription = subscription else{return}MyClient.shared.unsubscribe(from: subscription)self.subscription =nil}privatefunc handleMessage(_ message:ACMessage){switch message.type {case.confirmSubscription:print("ChatChannel subscribed")case.rejectSubscription:print("Server rejected ChatChannel subscription")default:break}}}Subscriptions are resubscribed on reconnection, so beware that .confirmSubscription may be called multiple times per subscription.
Register your Decodable messages
ACActionCable automatically decodes your models. For example, if your server broadcasts the following message:
{
"identifier": "{\"channel\":\"ChatChannel\",\"room_id\":42}",
"message": {
"my_object": {
"sender_id": 311,
"text": "Hello, room 42!",
"sent_at": 1600545466.294104
}
}
}Then ACActionCable provides two different approaches to automatically decode it:
// MyObject.swift
structMyObject:Codable{ // Must implement Decodable or Codable
letsenderId:Intlettext:StringletsentAt:Date}All you have to do is register the Codable struct for a single key. The name of the struct must match the name of that single key.
// MyClient.swift
privateinit(){
// Decode the single object for key `my_object` within `message`
ACMessageBodySingleObject.register(type:MyObject.self)}// ChatChannel.swift
privatefunc handleMessage(_ message:ACMessage){switch(message.type, message.body){case(.confirmSubscription, _):print("ChatChannel subscribed")case(.rejectSubscription, _):print("Server rejected ChatChannel subscription")case(_,.object(let object)):switch object {caseletmyObject as MyObject:print("\(myObject.text.debugDescription) from Sender \(myObject.senderId) at \(myObject.sentAt)")
// "Hello, room 42!" from Sender 311 at 2020-09-19 19:57:46 +0000
default:print("Warning: ChatChannel ignored message")}default:break}}If the message object sent from the server contains more than a single key, you have another option for automatic decoding:
// MessageType.swift
structMessageType:Codable{ // Must implement Decodable or Codable
letmyObject:MyObject}structMyObject:Codable{ // Must implement Decodable or Codable
letsenderId:Intlettext:StringletsentAt:Date}All you have to do is register the Codable struct for the whole message object.
// MyClient.swift
privateinit(){
// Decode the whole `message` object
ACMessage.register(type:MessageType.self, forChannelIdentifier: channelIdentifier)}The channelIdentifier must match the one the handler is subscribing to, because all incoming message objects for that channel will be decoded according to MessageType.
// ChatChannel.swift
privatefunc handleMessage(_ message:ACMessage){switch(message.type, message.body){case(.confirmSubscription, _):print("ChatChannel subscribed")case(.rejectSubscription, _):print("Server rejected ChatChannel subscription")case(_,.object(let object)):switch object {caseletmessage as MessageType:print("\(message.myObject.text.debugDescription) from Sender \(message.myObject.senderId) at \(message.myObject.sentAt)")
// "Hello, room 42!" from Sender 311 at 2020-09-19 19:57:46 +0000
default:print("Warning: ChatChannel ignored message")}default:break}}ACActionCable also provides access to the raw message body data for more involved processing:
// ChatChannel.swift
privatefunc handleMessage(_ message:ACMessage){switch(message.type){case(.confirmSubscription):print("ChatChannel subscribed")case(.rejectSubscription):print("Server rejected ChatChannel subscription")default:guardlet bodyData = message.bodyData else{return}
// do something fancy with the raw `Data`
}}ACActionCable automatically encodes your Encodable objects too:
// MyObject.swift
structMyObject:Codable{ // Must implement Encodable or Codable
letaction:StringletsenderId:Intlettext:StringletsentAt:Date}// ChatChannel.swift
func speak(_ text:String){letmyObject=MyObject(action:"speak", senderId:99, text: text, sentAt:Date())
subscription?.send(object: myObject)}Calling channel.speak("my message") would cause the following to be sent:
{
"command": "message",
"data": "{\"action\":\"speak\",\"my_object\":{\"sender_id\":99,\"sent_at\":1600545466.294104,\"text\":\"my message\"}}",
"identifier": "{\"channel\":\"ChatChannel\",\"room_id\":42}"
}By default, Date objects are encoded or decoded using .secondsSince1970. If you need to change to another format:
ACCommand.encoder.dateEncodingStrategy =.iso8601 // for dates like "2020-09-19T20:09:04Z"
ACMessage.decoder.dateDecodingStrategy =.iso8601Note that .iso8601 is quite strict and doesn't allow fractional seconds. If you need them, consider using .secondsSince1970, millisecondsSince1970, .formatted, or .custom.
If you need to listen to the internal state of ACClient, use ACClientTap.
// MyClient.swift
privateinit(){
// ...
lettap=ACClientTap(
onConnected:{(headers)inprint("Client connected with headers: \(headers.debugDescription)")}, onDisconnected:{(reason)inprint("Client disconnected with reason: \(reason.debugDescription)")}, onText:{(text)inprint("Client received text: \(text)")}, onMessage:{(message)inprint("Client received message: \(message)")})
client.add(tap)}Instead of opening an issue, please fix it yourself and then create a pull request. Please add new tests for your feature or fix, and don't forget to make sure that all the tests pass!