{"id":14955953,"url":"https://github.com/high5apps/acactioncable","last_synced_at":"2025-10-06T09:31:25.276Z","repository":{"id":56900146,"uuid":"296469944","full_name":"High5Apps/ACActionCable","owner":"High5Apps","description":"ACActionCable is a Swift 5 client for Ruby on Rails 6's Action Cable WebSocket server. It aims to be well-tested, dependency-free, and easy to use.","archived":false,"fork":false,"pushed_at":"2024-06-25T17:21:22.000Z","size":181,"stargazers_count":1,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-14T11:54:57.579Z","etag":null,"topics":["actioncable","rails6","ruby-on-rails","swift","swift5"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/High5Apps.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-18T00:09:59.000Z","updated_at":"2024-06-25T17:18:54.000Z","dependencies_parsed_at":"2024-06-01T17:45:44.750Z","dependency_job_id":null,"html_url":"https://github.com/High5Apps/ACActionCable","commit_stats":{"total_commits":136,"total_committers":3,"mean_commits":"45.333333333333336","dds":"0.32352941176470584","last_synced_commit":"f653580210eb49b6b6c7ab8c801696ebddd6ec7d"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/High5Apps%2FACActionCable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/High5Apps%2FACActionCable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/High5Apps%2FACActionCable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/High5Apps%2FACActionCable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/High5Apps","download_url":"https://codeload.github.com/High5Apps/ACActionCable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":235515793,"owners_count":19002548,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["actioncable","rails6","ruby-on-rails","swift","swift5"],"created_at":"2024-09-24T13:12:04.747Z","updated_at":"2025-10-06T09:31:19.907Z","avatar_url":"https://github.com/High5Apps.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ACActionCable\n\n[ACActionCable](https://github.com/High5Apps/ACActionCable) is a Swift 5 client for [Ruby on Rails](https://rubyonrails.org/) 6's [Action Cable](https://guides.rubyonrails.org/action_cable_overview.html) WebSocket server. It is a hard fork of [Action-Cable-Swift](https://github.com/nerzh/Action-Cable-Swift). It aims to be well-tested, dependency-free, and easy to use.\n\n## Installation\n\n### CocoaPods\n\nIf your project doesn't use [CocoaPods](https://cocoapods.org/) yet, [follow this guide](https://guides.cocoapods.org/using/using-cocoapods.html).\n\nAdd the following line to your `Podfile`:\n\n```ruby\npod 'ACActionCable', '~\u003e 2'\n```\n\nACActionCable uses [semantic versioning](https://semver.org/).\n\n## Usage\n\n### Implement ACWebSocketProtocol\n\nYou can use ACActionCable with any WebSocket library you'd like. Just create a class that implements [`ACWebSocketProtocol`](https://github.com/High5Apps/ACActionCable/blob/master/Sources/ACActionCable/ACWebSocketProtocol.swift). If you use [Starscream](https://github.com/daltoniam/Starscream), you can just copy [`ACStarscreamWebSocket`](https://github.com/High5Apps/ACActionCable/blob/master/Examples/ACStarscreamWebSocket.swift) into your project.\n\n### Create a [singleton](https://en.wikipedia.org/wiki/Singleton_pattern) class to hold an ACClient\n\n```swift\n// MyClient.swift\n\nimport ACActionCable\n\nclass MyClient {\n\n    static let shared = MyClient()\n    private let client: ACClient\n\n    private init() {\n        let socket = ACStarscreamWebSocket(stringURL: \"https://myrailsapp.com/cable\") // Your concrete implementation of ACWebSocketProtocol (see above)\n        client = ACClient(socket: socket, connectionMonitorTimeout: 6)\n    }\n}\n```\n\nIf you set a `connectionMonitorTimeout` and no ping is received for that many seconds, then [`ACConnectionMonitor`](https://github.com/High5Apps/ACActionCable/blob/master/Sources/ACActionCable/ACConnectionMonitor.swift) will periodically attempt to reconnect. Leave `connectionMonitorTimeout` nil to disable connection monitoring.\n\n### Connect and disconnect\n\nYou can set custom headers based on your server's requirements\n\n```swift\n// MyClient.swift\n\nfunc connect() {\n    client.headers = [\n        \"Auth\": \"Token\",\n        \"Origin\": \"https://myrailsapp.com\",\n    ]\n    client.connect()\n}\n\nfunc disconnect() {\n    client.disconnect()\n}\n```\n\nYou probably want to connect when the user's session begins and disconnect when the user logs out.\n\n```swift\n// User.swift\n\nfunc onSessionCreated() {\n    MyClient.shared.connect()\n    // ...\n}\n\nfunc logOut() {\n    // ...\n    MyClient.shared.disconnect()\n}\n```\n\n### Subscribe and unsubscribe\n\n```swift\n// MyClient.swift\n\nfunc subscribe(to channelIdentifier: ACChannelIdentifier, with messageHandler: @escaping ACMessageHandler) -\u003e ACSubscription? {\n    guard let subscription = client.subscribe(to: channelIdentifier, with: messageHandler) else {\n        print(\"Warning: MyClient ignored attempt to double subscribe. You are already subscribed to \\(channelIdentifier)\")\n        return nil\n    }\n    return subscription\n}\n\nfunc unsubscribe(from subscription: ACSubscription) {\n    client.unsubscribe(from: subscription)\n}\n```\n\n```swift\n// ChatChannel.swift\n\nimport ACActionCable\n\nclass ChatChannel {\n\n    private var subscription: ACSubscription?\n\n    func subscribe(to roomId: Int) {\n        guard subscription == nil else { return }\n        let channelIdentifier = ACChannelIdentifier(channelName: \"ChatChannel\", identifier: [\"room_id\": roomId])!\n        subscription = MyClient.shared.subscribe(to: channelIdentifier, with: handleMessage(_:))\n    }\n\n    func unsubscribe() {\n        guard let subscription = subscription else { return }\n        MyClient.shared.unsubscribe(from: subscription)\n        self.subscription = nil\n    }\n\n    private func handleMessage(_ message: ACMessage) {\n        switch message.type {\n        case .confirmSubscription:\n            print(\"ChatChannel subscribed\")\n        case .rejectSubscription:\n            print(\"Server rejected ChatChannel subscription\")\n        default:\n            break\n        }\n    }\n}\n```\n\nSubscriptions are resubscribed on reconnection, so beware that `.confirmSubscription` may be called multiple times per subscription.\n\n### Register your [`Decodable`](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) messages\n\nACActionCable automatically decodes your models. For example, if your server broadcasts the following message:\n\n```json\n{\n  \"identifier\": \"{\\\"channel\\\":\\\"ChatChannel\\\",\\\"room_id\\\":42}\",\n  \"message\": {\n    \"my_object\": {\n      \"sender_id\": 311,\n      \"text\": \"Hello, room 42!\",\n      \"sent_at\": 1600545466.294104\n    }\n  }\n}\n```\n\nThen ACActionCable provides two different approaches to automatically decode it:\n\n#### A) Automatically decode single key of `message`\n\n```swift\n// MyObject.swift\n\nstruct MyObject: Codable { // Must implement Decodable or Codable\n    let senderId: Int\n    let text: String\n    let sentAt: Date\n}\n```\n\nAll 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.\n\n```swift\n// MyClient.swift\n\nprivate init() {\n  // Decode the single object for key `my_object` within `message`\n  ACMessageBodySingleObject.register(type: MyObject.self)\n}\n```\n\n```swift\n// ChatChannel.swift\n\nprivate func handleMessage(_ message: ACMessage) {\n    switch (message.type, message.body) {\n    case (.confirmSubscription, _):\n        print(\"ChatChannel subscribed\")\n    case (.rejectSubscription, _):\n        print(\"Server rejected ChatChannel subscription\")\n    case (_, .object(let object)):\n        switch object {\n        case let myObject as MyObject:\n            print(\"\\(myObject.text.debugDescription) from Sender \\(myObject.senderId) at \\(myObject.sentAt)\")\n            // \"Hello, room 42!\" from Sender 311 at 2020-09-19 19:57:46 +0000\n        default:\n            print(\"Warning: ChatChannel ignored message\")\n        }\n    default:\n        break\n    }\n}\n```\n\nIf the `message` object sent from the server contains more than a single key, you have another option for automatic decoding:\n\n#### B) Automatically decode whole `message` object\n\n```swift\n// MessageType.swift\n\nstruct MessageType: Codable { // Must implement Decodable or Codable\n    let myObject: MyObject\n}\n\nstruct MyObject: Codable { // Must implement Decodable or Codable\n    let senderId: Int\n    let text: String\n    let sentAt: Date\n}\n```\n\nAll you have to do is register the `Codable` struct for the whole `message` object.\n\n```swift\n// MyClient.swift\n\nprivate init() {\n  // Decode the whole `message` object\n  ACMessage.register(type: MessageType.self, forChannelIdentifier: channelIdentifier)\n}\n```\n\nThe `channelIdentifier` must match the one the handler is subscribing to, because all incoming `message` objects for that channel will be decoded according to `MessageType`.\n\n```swift\n// ChatChannel.swift\n\nprivate func handleMessage(_ message: ACMessage) {\n    switch (message.type, message.body) {\n    case (.confirmSubscription, _):\n        print(\"ChatChannel subscribed\")\n    case (.rejectSubscription, _):\n        print(\"Server rejected ChatChannel subscription\")\n    case (_, .object(let object)):\n        switch object {\n        case let message as MessageType:\n            print(\"\\(message.myObject.text.debugDescription) from Sender \\(message.myObject.senderId) at \\(message.myObject.sentAt)\")\n            // \"Hello, room 42!\" from Sender 311 at 2020-09-19 19:57:46 +0000\n        default:\n            print(\"Warning: ChatChannel ignored message\")\n        }\n    default:\n        break\n    }\n}\n```\n\n### Accessing the raw message body data\n\nACActionCable also provides access to the raw message body data for more involved processing:\n\n```swift\n// ChatChannel.swift\n\nprivate func handleMessage(_ message: ACMessage) {\n    switch (message.type) {\n    case (.confirmSubscription):\n        print(\"ChatChannel subscribed\")\n    case (.rejectSubscription):\n        print(\"Server rejected ChatChannel subscription\")\n    default:\n        guard let bodyData = message.bodyData else {\n            return\n        }\n\n        // do something fancy with the raw `Data`\n    }\n}\n```\n\n### Send messages\n\nACActionCable automatically encodes your [`Encodable`](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) objects too:\n\n```swift\n// MyObject.swift\n\nstruct MyObject: Codable { // Must implement Encodable or Codable\n    let action: String\n    let senderId: Int\n    let text: String\n    let sentAt: Date\n}\n```\n\n```swift\n// ChatChannel.swift\n\nfunc speak(_ text: String) {\n    let myObject = MyObject(action: \"speak\", senderId: 99, text: text, sentAt: Date())\n    subscription?.send(object: myObject)\n}\n```\n\nCalling `channel.speak(\"my message\")` would cause the following to be sent:\n\n```json\n{\n  \"command\": \"message\",\n  \"data\": \"{\\\"action\\\":\\\"speak\\\",\\\"my_object\\\":{\\\"sender_id\\\":99,\\\"sent_at\\\":1600545466.294104,\\\"text\\\":\\\"my message\\\"}}\",\n  \"identifier\": \"{\\\"channel\\\":\\\"ChatChannel\\\",\\\"room_id\\\":42}\"\n}\n```\n\n### (Optional) Modify encoder/decoder date formatting\n\nBy default, `Date` objects are encoded or decoded using [`.secondsSince1970`](https://developer.apple.com/documentation/foundation/jsonencoder/dateencodingstrategy/secondssince1970). If you need to change to another format:\n\n```swift\nACCommand.encoder.dateEncodingStrategy = .iso8601 // for dates like \"2020-09-19T20:09:04Z\"\nACMessage.decoder.dateDecodingStrategy = .iso8601\n```\n\nNote that `.iso8601` is quite strict and doesn't allow fractional seconds. If you need them, consider using `.secondsSince1970`, `millisecondsSince1970`, `.formatted`, or `.custom`.\n\n### (Optional) Add an ACClientTap\n\nIf you need to listen to the internal state of `ACClient`, use `ACClientTap`.\n\n```swift\n// MyClient.swift\n\nprivate init() {\n    // ...\n    let tap = ACClientTap(\n        onConnected: { (headers) in\n            print(\"Client connected with headers: \\(headers.debugDescription)\")\n        }, onDisconnected: { (reason) in\n            print(\"Client disconnected with reason: \\(reason.debugDescription)\")\n        }, onText: { (text) in\n            print(\"Client received text: \\(text)\")\n        }, onMessage: { (message) in\n            print(\"Client received message: \\(message)\")\n        })\n    client.add(tap)\n}\n```\n\n## Contributing\n\nInstead of opening an issue, please fix it yourself and then [create a pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). Please add new tests for your feature or fix, and don't forget to make sure that all the tests pass!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhigh5apps%2Facactioncable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhigh5apps%2Facactioncable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhigh5apps%2Facactioncable/lists"}