├── .DS_Store ├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── SCTestSever └── scserver ├── ScClient.podspec ├── Sources ├── Main │ └── main.swift └── ScClient │ ├── Emitter │ └── Listener.swift │ ├── Models │ └── Event.swift │ ├── Parser │ └── Parser.swift │ ├── Utils │ ├── AtomicInteger.swift │ ├── ClientUtils.swift │ └── Miscellaneous.swift │ └── client.swift ├── Tests ├── Integration │ ├── EmitReceiveTest.swift │ ├── PubSubTest.swift │ └── SetUp │ │ ├── ClientBuilder.swift │ │ ├── ClientConfig.swift │ │ ├── ClientPool.swift │ │ └── ClientPoolExecutor.swift ├── ScClientTestSuite └── Unit │ ├── ClientUtilsTest.swift │ ├── MiscellaneousTest.swift │ ├── ParserTest.swift │ └── UtilsTest.swift └── commands.txt /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sacOO7/socketcluster-client-swift/e97629e8cebfd7acf0b882856464a063327e4174/.DS_Store -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # For a detailed guide to building and testing on iOS, read the docs: 2 | # https://circleci.com/docs/2.0/testing-ios/ 3 | 4 | version: 2.1 5 | 6 | jobs: 7 | build: 8 | 9 | macos: 10 | xcode: 11.3.1 # Specify the Xcode version to use 11 | 12 | steps: 13 | - checkout 14 | - run: swift package resolve # install all dependencies 15 | - run: swift build # build the package 16 | - run: swift test #test the package -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | *.xcodeproj 8 | 9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 10 | *.xcscmblueprint 11 | *.xccheckout 12 | 13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 14 | build/ 15 | DerivedData/ 16 | *.moved-aside 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | 29 | ## App packaging 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | # *.xcodeproj 45 | # 46 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 47 | # hence it is not needed unless you have added a package configuration file to your project 48 | .swiftpm 49 | 50 | .build/ 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # 58 | # Pods/ 59 | # 60 | # Add this line if you want to avoid checking in source code from the Xcode workspace 61 | # *.xcworkspace 62 | 63 | # Carthage 64 | # 65 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 66 | # Carthage/Checkouts 67 | 68 | Carthage/Build/ 69 | 70 | # Accio dependency management 71 | Dependencies/ 72 | .accio/ 73 | 74 | # fastlane 75 | # 76 | # It is recommended to not store the screenshots in the git repo. 77 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 78 | # For more information about the recommended setup visit: 79 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 80 | 81 | fastlane/report.xml 82 | fastlane/Preview.html 83 | fastlane/screenshots/**/*.png 84 | fastlane/test_output 85 | 86 | # Code Injection 87 | # 88 | # After new code Injection tools there's a generated folder /iOSInjectionProject 89 | # https://github.com/johnno1962/injectionforxcode 90 | 91 | iOSInjectionProject/ 92 | 93 | SCTestSever/sc-testserver -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "HandyJSON", 6 | "repositoryURL": "https://github.com/alibaba/HandyJSON.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "dfab665e87f5dd56e16273222718ecd04a9a9d70", 10 | "version": "5.0.1" 11 | } 12 | }, 13 | { 14 | "package": "Nimble", 15 | "repositoryURL": "https://github.com/Quick/Nimble.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "f8657642dfdec9973efc79cc68bcef43a653a2bc", 19 | "version": "8.0.2" 20 | } 21 | }, 22 | { 23 | "package": "Quick", 24 | "repositoryURL": "https://github.com/Quick/Quick.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "33682c2f6230c60614861dfc61df267e11a1602f", 28 | "version": "2.2.0" 29 | } 30 | }, 31 | { 32 | "package": "RxSwift", 33 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "b3e888b4972d9bc76495dd74d30a8c7fad4b9395", 37 | "version": "5.0.1" 38 | } 39 | }, 40 | { 41 | "package": "Starscream", 42 | "repositoryURL": "https://github.com/daltoniam/Starscream.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "e6b65c6d9077ea48b4a7bdda8994a1d3c6969c8d", 46 | "version": "3.1.1" 47 | } 48 | }, 49 | { 50 | "package": "swift-nio-zlib-support", 51 | "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", 55 | "version": "1.0.0" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ScClient", 8 | products: [ 9 | .library( 10 | name: "ScClient", 11 | targets: ["ScClient"]), 12 | ], 13 | dependencies: [ 14 | // Dependencies declare other packages that this package depends on. 15 | .package(url: "https://github.com/daltoniam/Starscream.git", .exact("3.1.1")), 16 | .package(url: "https://github.com/alibaba/HandyJSON.git", .exact("5.0.1")), 17 | .package(url: "https://github.com/Quick/Quick.git", .exact("2.2.0")), 18 | .package(url: "https://github.com/Quick/Nimble.git", .exact("8.0.2")), 19 | .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "5.0.0") 20 | ], 21 | targets: [ 22 | .target( 23 | name: "ScClient", 24 | dependencies: [ 25 | "Starscream", 26 | "HandyJSON" 27 | ]), 28 | .target( 29 | name: "Main", 30 | dependencies: [ 31 | "ScClient", 32 | ]), 33 | .testTarget( 34 | name: "Integration", 35 | dependencies: [ 36 | "ScClient", 37 | "Quick", 38 | "Nimble", 39 | "RxSwift" 40 | ]), 41 | .testTarget( 42 | name: "Unit", 43 | dependencies: [ 44 | "ScClient" 45 | ]) 46 | ] 47 | ) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # socketcluster-client-swift 2 | Native iOS/macOS client written in swift 3 | 4 | Overview 5 | -------- 6 | This client provides following functionality 7 | 8 | - Easy to setup and use 9 | - Support for emitting and listening to remote events 10 | - Pub/sub 11 | - Authentication (JWT) 12 | 13 | Client supports following platforms 14 | 15 | - iOS >= 8.0 16 | - macOS>= 10.10 17 | - watchOS >= 2.0 18 | - tvOS >= 9.0 19 | 20 | Installation and Use 21 | -------------------- 22 | 23 | ### [CocoaPods](http://cocoapods.org) 24 | 25 | ```ruby 26 | pod 'ScClient' 27 | ``` 28 | 29 | ### Swift Package Manager 30 | 31 | - To install add this to depedencies section of Package.swift 32 | 33 | ```swift 34 | dependencies: [ 35 | // other dependencies 36 | .package(url: "https://github.com/sacOO7/ScClient", from: "2.0.1") 37 | ] 38 | ``` 39 | - To use the library add this to target dependencies 40 | 41 | ```swift 42 | targets: [ 43 | .target( 44 | name: "tool", 45 | dependencies: [ 46 | "ScClient" 47 | ]) 48 | ] 49 | ``` 50 | 51 | Description 52 | ----------- 53 | Create instance of `scclient` by passing url of socketcluster-server end-point 54 | 55 | ```swift 56 | //Create a client instance 57 | var client = ScClient(url: "http://localhost:8000/socketcluster/") 58 | 59 | ``` 60 | **Important Note** : Default url to socketcluster end-point is always *ws://somedomainname.com/socketcluster/*. 61 | 62 | #### Registering basic listeners 63 | 64 | - Different closure functions are given as an argument to register listeners 65 | - Example : main.swift 66 | 67 | ```swift 68 | import Foundation 69 | import ScClient 70 | 71 | var client = ScClient(url: "http://localhost:8000/socketcluster/") 72 | 73 | var onConnect = { 74 | (client :ScClient) in 75 | print("Connnected to server") 76 | } 77 | 78 | var onDisconnect = { 79 | (client :ScClient, error : Error?) in 80 | print("Disconnected from server due to ", error?.localizedDescription) 81 | } 82 | 83 | var onAuthentication = { 84 | (client :ScClient, isAuthenticated : Bool?) in 85 | print("Authenticated is ", isAuthenticated) 86 | startCode(client : client) 87 | } 88 | 89 | var onSetAuthentication = { 90 | (client : ScClient, token : String?) in 91 | print("Token is ", token) 92 | } 93 | client.setBasicListener(onConnect: onConnect, onConnectError: nil, onDisconnect: onDisconnect) 94 | client.setAuthenticationListener(onSetAuthentication: onSetAuthentication, onAuthentication: onAuthentication) 95 | 96 | client.connect() 97 | 98 | while(true) { 99 | RunLoop.current.run(until: Date()) 100 | usleep(10) 101 | } 102 | 103 | func startCode(client scclient.Client) { 104 | // start writing your code from here 105 | // All emit, receive and publish events 106 | } 107 | 108 | ``` 109 | 110 | 111 | #### Connecting to server 112 | 113 | - For connecting to server: 114 | 115 | ```swift 116 | //This will send websocket handshake request to socketcluster-server 117 | client.connect() 118 | ``` 119 | 120 | #### Getting connection status 121 | 122 | ```swift 123 | //This will send websocket handshake request to socketcluster-server 124 | var status = client.isConnected() 125 | ``` 126 | 127 | 128 | Emitting and listening to events 129 | -------------------------------- 130 | #### Event emitter 131 | 132 | - eventname is name of event and message can be String, boolean, Int or Object 133 | 134 | ```swift 135 | 136 | client.emit(eventName: eventname, data: message as AnyObject) 137 | 138 | //client.emit(eventName: "chat", data: "This is my sample message" as AnyObject) 139 | 140 | ``` 141 | 142 | - To send event with acknowledgement 143 | 144 | ```swift 145 | 146 | client.emitAck(eventName: "chat", data: "This is my sample message" as AnyObject, ack : { 147 | (eventName : String, error : AnyObject? , data : AnyObject?) in 148 | print("Got data for eventName ", eventName, " error is ", error, " data is ", data) 149 | }) 150 | 151 | ``` 152 | 153 | #### Event Listener 154 | 155 | - For listening to events : 156 | 157 | The object received can be String, Boolean, Int or Object 158 | 159 | ```swift 160 | // Receiver code without sending acknowledgement back 161 | client.on(eventName: "yell", ack: { 162 | (eventName : String, data : AnyObject?) in 163 | print("Got data for eventName ", eventName, " data is ", data) 164 | }) 165 | 166 | ``` 167 | 168 | - To send acknowledgement back to server 169 | 170 | ```swift 171 | // Receiver code with ack 172 | client.onAck(eventName: "yell", ack: { 173 | (eventName : String, data : AnyObject?, ack : (AnyObject?, AnyObject?) -> Void) in 174 | print("Got data for eventName ", eventName, " data is ", data) 175 | ack("This is error " as AnyObject, "This is data " as AnyObject) 176 | }) 177 | 178 | ``` 179 | 180 | 181 | 182 | Implementing Pub-Sub via channels 183 | --------------------------------- 184 | 185 | #### Creating channel 186 | 187 | - For creating and subscribing to channels: 188 | 189 | ```swift 190 | // without acknowledgement 191 | client.subscribe(channelName: "yell") 192 | 193 | //with acknowledgement 194 | client.subscribeAck(channelName: "yell", ack : { 195 | (channelName : String, error : AnyObject?, data : AnyObject?) in 196 | if (error is NSNull) { 197 | print("Successfully subscribed to channel ", channelName) 198 | } else { 199 | print("Got error while subscribing ", error) 200 | } 201 | }) 202 | ``` 203 | 204 | 205 | #### Publishing event on channel 206 | 207 | - For publishing event : 208 | 209 | ```swift 210 | 211 | // without acknowledgement 212 | client.publish(channelName: "yell", data: "I am sending data to yell" as AnyObject) 213 | 214 | 215 | // with acknowledgement 216 | client.publishAck(channelName: "yell", data: "I am sending data to yell" as AnyObject, ack : { 217 | (channelName : String, error : AnyObject?, data : AnyObject?) in 218 | if (error is NSNull) { 219 | print("Successfully published to channel ", channelName) 220 | }else { 221 | print("Got error while publishing ", error) 222 | } 223 | }) 224 | ``` 225 | 226 | #### Listening to channel 227 | 228 | - For listening to channel event : 229 | 230 | ```swift 231 | client.onChannel(channelName: "yell", ack: { 232 | (channelName : String , data : AnyObject?) in 233 | print ("Got data for channel", channelName, " object data is ", data) 234 | }) 235 | ``` 236 | 237 | #### Un-subscribing to channel 238 | 239 | ```swift 240 | // without acknowledgement 241 | client.unsubscribe(channelName: "yell") 242 | 243 | //with acknowledgement 244 | client.unsubscribeAck(channelName: "yell", ack : { 245 | (channelName : String, error : AnyObject?, data : AnyObject?) in 246 | if (error is NSNull) { 247 | print("Successfully unsubscribed to channel ", channelName) 248 | } else { 249 | print("Got error while unsubscribing ", error) 250 | } 251 | }) 252 | ``` 253 | 254 | #### Disable SSL Certificate Verification 255 | 256 | ```swift 257 | var client = ScClient(url: "http://localhost:8000/socketcluster/") 258 | client.disableSSLVerification(true) 259 | ``` 260 | ### Custom Queue 261 | 262 | A custom queue can be specified when delegate methods are called. By default `DispatchQueue.main` is used, thus making all delegate methods calls run on the main thread. It is important to note that all WebSocket processing is done on a background thread, only the delegate method calls are changed when modifying the queue. The actual processing is always on a background thread and will not pause your app. 263 | 264 | ```swift 265 | var client = ScClient(url: "http://localhost:8000/socketcluster/") 266 | //create a custom queue 267 | client.setBackgroundQueue(queueName : "com.example.chatapp") 268 | ``` 269 | 270 | ### Custom Headers 271 | 272 | You can also override the default websocket headers with your own custom ones like so: 273 | 274 | ```swift 275 | var request = URLRequest(url: URL(string: "http://localhost:8000/socketcluster/")!) 276 | request.timeoutInterval = 5 277 | request.setValue("someother protocols", forHTTPHeaderField: "Sec-WebSocket-Protocol") 278 | request.setValue("14", forHTTPHeaderField: "Sec-WebSocket-Version") 279 | request.setValue("Everything is Awesome!", forHTTPHeaderField: "My-Awesome-Header") 280 | var client = ScClient(URLRequest: request) 281 | ``` 282 | 283 | ### Custom HTTP Method 284 | 285 | Your server may use a different HTTP method when connecting to the websocket: 286 | 287 | ```swift 288 | var request = URLRequest(url: URL(string: "http://localhost:8000/socketcluster/")!) 289 | request.httpMethod = "POST" 290 | request.timeoutInterval = 5 291 | var client = ScClient(URLRequest: request) 292 | ``` 293 | 294 | ### Protocols 295 | 296 | If you need to specify a protocol, simple add it to the init: 297 | 298 | ```swift 299 | //chat and superchat are the example protocols here 300 | var request = URLRequest(url: URL(string: "http://localhost:8000/socketcluster/")!) 301 | var client = ScClient(URLRequest: request, protocols: ["chat","superchat"]) 302 | ``` 303 | -------------------------------------------------------------------------------- /SCTestSever/scserver: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if test "$#" -ne 1; then 4 | echo "Please provide start or stop as argument" 5 | exit 1 6 | fi 7 | 8 | FOLDER="sc-testserver" 9 | URL="https://github.com/sacOO7/sc-testserver" 10 | 11 | if [ ! -d "$FOLDER" ] ; then 12 | git clone $URL $FOLDER 13 | cd "$FOLDER" 14 | else 15 | cd "$FOLDER" 16 | git pull $URL 17 | fi 18 | 19 | if [ $1 = "start" ]; then 20 | ./runserver 21 | fi 22 | if [ $1 = "stop" ]; then 23 | ./stopserver 24 | fi -------------------------------------------------------------------------------- /ScClient.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint ScClient.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | s.name = "ScClient" 19 | s.version = "2.0.1" 20 | s.summary = "A socketcluster client for iOS and OSX." 21 | s.swift_version = '4.0' 22 | 23 | # This description is used to generate tags and improve search results. 24 | # * Think: What does it do? Why did you write it? What is the focus? 25 | # * Try to keep it short, snappy and to the point. 26 | # * Write the description between the DESC delimiters below. 27 | # * Finally, don't worry about the indent, CocoaPods strips it! 28 | s.description = "Native iOS/macOS client written in swift. Provides support to for emitting and listening to remote events, publish-subscribe and authentication using JWT" 29 | 30 | s.homepage = "https://github.com/sacOO7/socketcluster-client-swift.git" 31 | # s.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif" 32 | 33 | 34 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 35 | # 36 | # Licensing your code is important. See http://choosealicense.com for more info. 37 | # CocoaPods will detect a license file if there is a named LICENSE* 38 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 39 | # 40 | 41 | # s.license = "Apache-2.0" 42 | s.license = { :type => "Apache", :file => "LICENSE" } 43 | 44 | 45 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 46 | # 47 | # Specify the authors of the library, with email addresses. Email addresses 48 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 49 | # accepts just a name if you'd rather not provide an email address. 50 | # 51 | # Specify a social_media_url where others can refer to, for example a twitter 52 | # profile URL. 53 | # 54 | 55 | s.author = { "sacOO7" => "sachinshinde7676@gmail.com" } 56 | # Or just: s.author = "sacOO7" 57 | # s.authors = { "sacOO7" => "sachinshinde7676@gmail.com" } 58 | # s.social_media_url = "http://twitter.com/sacOO7" 59 | 60 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 61 | # 62 | # If this Pod runs only on iOS or OS X, then specify the platform and 63 | # the deployment target. You can optionally include the target after the platform. 64 | # 65 | 66 | # s.platform = :ios 67 | # s.platform = { :ios => '9.0' } 68 | 69 | s.platforms = { :ios => "8.0", :osx => "10.10", :watchos => "2.0", :tvos => "9.0" } 70 | 71 | # s.ios.deployment_target = '9.0' 72 | # s.osx.deployment_target = '10.10' 73 | 74 | # When using multiple platforms 75 | # s.ios.deployment_target = "5.0" 76 | # s.osx.deployment_target = "10.7" 77 | # s.watchos.deployment_target = "2.0" 78 | # s.tvos.deployment_target = "9.0" 79 | 80 | 81 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 82 | # 83 | # Specify the location from where the source should be retrieved. 84 | # Supports git, hg, bzr, svn and HTTP. 85 | # 86 | 87 | s.source = { :git => "https://github.com/sacOO7/socketcluster-client-swift.git", :tag => s.version.to_s } 88 | 89 | 90 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 91 | # 92 | # CocoaPods is smart about how it includes source code. For source files 93 | # giving a folder will include any swift, h, m, mm, c & cpp files. 94 | # For header files it will include any header in the folder. 95 | # Not including the public_header_files will make all headers public. 96 | # 97 | 98 | s.source_files = "Sources/ScClient/**/*" 99 | # s.exclude_files = "Classes/Exclude" 100 | 101 | # s.public_header_files = "Classes/**/*.h" 102 | 103 | 104 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 105 | # 106 | # A list of resources included with the Pod. These are copied into the 107 | # target bundle with a build phase script. Anything else will be cleaned. 108 | # You can preserve files from being cleaned, please don't preserve 109 | # non-essential files like tests, examples and documentation. 110 | # 111 | 112 | # s.resource = "icon.png" 113 | # s.resources = "Resources/*.png" 114 | 115 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave" 116 | 117 | 118 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 119 | # 120 | # Link your library with frameworks, or libraries. Libraries do not include 121 | # the lib prefix of their name. 122 | # 123 | 124 | # s.framework = "SomeFramework" 125 | # s.frameworks = "SomeFramework", "AnotherFramework" 126 | # s.library = "iconv" 127 | # s.libraries = "iconv", "xml2" 128 | 129 | 130 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 131 | # 132 | # If your library depends on compiler flags you can set them in the xcconfig hash 133 | # where they will only apply to your library. If you depend on other Podspecs 134 | # you can include multiple dependencies to ensure it works. 135 | 136 | s.requires_arc = true 137 | 138 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 139 | s.dependency "Starscream", "~> 3.1.1" 140 | s.dependency "HandyJSON", "~> 5.0.1" 141 | end 142 | -------------------------------------------------------------------------------- /Sources/Main/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ScClient 3 | 4 | var client = ScClient(url: "http://localhost:8000/socketcluster/") 5 | 6 | var onConnect = { 7 | (client :ScClient) in 8 | print("Connnected to server") 9 | } 10 | 11 | var onDisconnect = { 12 | (client :ScClient, error : Error?) in 13 | print("Disconnected from server due to ", error?.localizedDescription) 14 | } 15 | 16 | var onAuthentication = { 17 | (client :ScClient, isAuthenticated : Bool?) in 18 | print("Authenticated is ", isAuthenticated) 19 | } 20 | 21 | var onSetAuthentication = { 22 | (client : ScClient, token : String?) in 23 | print("Token is ", token) 24 | client.subscribeAck(channelName: "yell", ack : { 25 | (channelName : String, error : AnyObject?, data : AnyObject?) in 26 | if (error is NSNull) { 27 | print("Successfully subscribed to channel ", channelName) 28 | } else { 29 | print("Got error while subscribing ", error) 30 | } 31 | 32 | }) 33 | 34 | client.publishAck(channelName: "yell", data: "I am sending data to yell" as AnyObject, ack : { 35 | (channelName : String, error : AnyObject?, data : AnyObject?) in 36 | if (error is NSNull) { 37 | print("Successfully published to channel ", channelName) 38 | }else { 39 | print("Got error while publishing ", error) 40 | } 41 | 42 | }) 43 | } 44 | 45 | client.setBasicListener(onConnect: onConnect, onConnectError: nil, onDisconnect: onDisconnect) 46 | client.setAuthenticationListener(onSetAuthentication: onSetAuthentication, onAuthentication: onAuthentication) 47 | 48 | client.onChannel(channelName: "yell", ack: { 49 | (channelName : String , data : AnyObject?) in 50 | print ("Got data for channel", channelName, " object data is ", data) 51 | }) 52 | 53 | client.connect() 54 | 55 | while(true) { 56 | RunLoop.current.run(until: Date()) 57 | usleep(10) 58 | } 59 | 60 | -------------------------------------------------------------------------------- /Sources/ScClient/Emitter/Listener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Listener.swift 3 | // ScClientPackageDescription 4 | // 5 | // Created by admin on 13/01/2018. 6 | // 7 | 8 | import Foundation 9 | public class Listener { 10 | var emitAckListener : [Int : (String, (String, AnyObject?, AnyObject? ) -> Void )] 11 | var onListener :[String : (String, AnyObject?) -> Void] 12 | var onAckListener : [String: (String, AnyObject?, (AnyObject?, AnyObject?) -> Void ) -> Void] 13 | 14 | public init() { 15 | emitAckListener = [Int : (String, (String, AnyObject?, AnyObject? ) -> Void )]() 16 | onListener = [String : (String, AnyObject?) -> Void]() 17 | onAckListener = [String: (String, AnyObject?, (AnyObject?, AnyObject?) -> Void ) -> Void]() 18 | } 19 | 20 | func putEmitAck(id : Int, eventName : String, ack : @escaping (String, AnyObject?, AnyObject? ) -> Void ) { 21 | self.emitAckListener[id] = (eventName, ack) 22 | } 23 | 24 | 25 | func handleEmitAck (id : Int, error : AnyObject?, data : AnyObject?) { 26 | if let ackobject = emitAckListener[id] { 27 | let eventName = ackobject.0 28 | let ack = ackobject.1 29 | ack(eventName, error, data) 30 | } 31 | } 32 | 33 | func putOnListener(eventName : String, onListener: @escaping (String, AnyObject?) -> Void) { 34 | self.onListener[eventName] = onListener 35 | } 36 | 37 | func handleOnListener (eventName : String, data : AnyObject?) { 38 | if let on = onListener[eventName] { 39 | on(eventName, data) 40 | } 41 | } 42 | 43 | func putOnAckListener(eventName : String, onAckListener : @escaping (String, AnyObject?, (AnyObject?, AnyObject?) -> Void ) -> Void) { 44 | self.onAckListener[eventName] = onAckListener 45 | } 46 | 47 | func handleOnAckListener (eventName : String, data : AnyObject?, ack : (AnyObject?, AnyObject?) -> Void) { 48 | if let onAck = onAckListener[eventName] { 49 | onAck(eventName, data, ack) 50 | } 51 | } 52 | 53 | func hasEventAck(eventName : String) -> Bool { 54 | return (onAckListener[eventName] != nil) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Sources/ScClient/Models/Event.swift: -------------------------------------------------------------------------------- 1 | import HandyJSON 2 | 3 | class EmitEvent: HandyJSON { 4 | 5 | var event: String! 6 | var data: AnyObject? 7 | var cid: Int! 8 | 9 | required init() { 10 | } 11 | 12 | init(event: String, data: AnyObject?, cid : Int) { 13 | self.event = event 14 | self.data = data 15 | self.cid = cid 16 | } 17 | 18 | } 19 | 20 | class ReceiveEvent : HandyJSON { 21 | var data : AnyObject? 22 | var error : AnyObject? 23 | var rid : Int! 24 | 25 | convenience init(data : AnyObject? , error : AnyObject?, rid : Int) { 26 | self.init() 27 | self.data = data 28 | self.error = error 29 | self.rid = rid 30 | } 31 | 32 | required init() { 33 | } 34 | } 35 | 36 | class Channel : HandyJSON{ 37 | var channel : String! 38 | var data : AnyObject? 39 | 40 | init(channel : String, data : AnyObject?) { 41 | self.channel = channel 42 | self.data = data 43 | } 44 | 45 | required init() { 46 | } 47 | } 48 | 49 | class AuthChannel : HandyJSON{ 50 | var channel : String! 51 | var data : ChannelData? 52 | 53 | init(channel : String, token: String?) { 54 | self.channel = channel 55 | self.data = ChannelData(jwt: token) 56 | } 57 | 58 | required init() { 59 | } 60 | } 61 | 62 | class ChannelData: HandyJSON { 63 | var jwt: String? 64 | 65 | init(jwt: String?) { 66 | self.jwt = jwt 67 | } 68 | 69 | required init() { 70 | } 71 | } 72 | 73 | class AuthData : HandyJSON{ 74 | var authToken : String? 75 | 76 | 77 | init(authToken : String?) { 78 | self.authToken = authToken 79 | } 80 | 81 | required init() { 82 | } 83 | } 84 | 85 | class HandShake : HandyJSON { 86 | 87 | 88 | var event : String! 89 | var data : AuthData! 90 | var cid : Int! 91 | 92 | 93 | init(event : String, data : AuthData, cid : Int) { 94 | self.event = event 95 | self.data = data 96 | self.cid = cid 97 | } 98 | 99 | required init() { 100 | } 101 | 102 | } 103 | 104 | class Model { 105 | 106 | public static func getEmitEventObject(eventName: String, data : AnyObject?, messageId : Int) -> EmitEvent{ 107 | return EmitEvent(event: eventName, data: data, cid: messageId) 108 | } 109 | 110 | public static func getReceiveEventObject(data : AnyObject?, error : AnyObject?, messageId : Int) -> ReceiveEvent{ 111 | return ReceiveEvent(data: data, error: error, rid: messageId) 112 | } 113 | 114 | public static func getChannelObject (data : AnyObject?) -> Channel? { 115 | if let channel = data as? [String : Any] { 116 | return Channel(channel: channel["channel"] as! String, data: channel["data"] as AnyObject) 117 | } 118 | return nil 119 | } 120 | 121 | public static func getSubscribeEventObject(channelName : String, messageId : Int) -> EmitEvent{ 122 | return EmitEvent(event: "#subscribe", data: Channel(channel: channelName, data :nil) as AnyObject, cid: messageId) 123 | } 124 | 125 | public static func getSubscribeEventObject(channelName : String, messageId : Int, token: String? = nil) -> EmitEvent{ 126 | return EmitEvent(event: "#subscribe", data: AuthChannel(channel: channelName, token: token) as AnyObject, cid: messageId) 127 | } 128 | 129 | public static func getUnsubscribeEventObject(channelName : String, messageId : Int) -> EmitEvent{ 130 | return EmitEvent(event: "#unsubscribe", data: channelName as AnyObject, cid: messageId) 131 | } 132 | 133 | public static func getPublishEventObject(channelName : String, data : AnyObject?, messageId : Int) -> EmitEvent{ 134 | return EmitEvent(event: "#publish", data: Channel(channel: channelName, data :data) as AnyObject, cid: messageId) 135 | } 136 | 137 | public static func getHandshakeObject(authToken : String?, messageId : Int) -> HandShake{ 138 | return HandShake(event: "#handshake", data: AuthData(authToken: authToken), cid: messageId) 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /Sources/ScClient/Parser/Parser.swift: -------------------------------------------------------------------------------- 1 | public enum MessageType { 2 | case isAuthenticated 3 | case publish 4 | case removeToken 5 | case setToken 6 | case event 7 | case ackReceive 8 | } 9 | 10 | public class Parser { 11 | 12 | public static func parse(rid : Int?, cid : Int?, event : String?) -> MessageType { 13 | if (event != nil) { 14 | if (event == "#publish") { 15 | return MessageType.publish 16 | } else if (event == "#removeAuthToken") { 17 | return MessageType.removeToken 18 | } else if (event == "#setAuthToken") { 19 | return MessageType.setToken 20 | } else { 21 | return MessageType.event 22 | } 23 | } else if (rid == 1) { 24 | return MessageType.isAuthenticated 25 | } else { 26 | return MessageType.ackReceive 27 | } 28 | } 29 | 30 | public static func getMessageDetails(myMessage : Any) -> (data: Any?, rid : Int?, cid : Int?, eventName : String?, error : Any?)? { 31 | if let messageItem = myMessage as? [String: Any] { 32 | let data = messageItem["data"] 33 | let rid = messageItem["rid"] as? Int 34 | let cid = messageItem["cid"] as? Int 35 | let event = messageItem["event"] as? String 36 | let error = messageItem["error"] 37 | return (data, rid, cid, event, error) 38 | } 39 | return nil 40 | 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Sources/ScClient/Utils/AtomicInteger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicInteger.swift 3 | // ScClient 4 | // 5 | // Created by admin on 07/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class AtomicInteger { 11 | 12 | private let lock = DispatchSemaphore(value: 1) 13 | private var _value: Int 14 | 15 | public init(value initialValue: Int = 0) { 16 | 17 | _value = initialValue 18 | } 19 | 20 | public var value: Int { 21 | get { 22 | lock.wait() 23 | defer { lock.signal() } 24 | return _value 25 | } 26 | set { 27 | lock.wait() 28 | defer { lock.signal() } 29 | _value = newValue 30 | } 31 | } 32 | 33 | public func decrementAndGet() -> Int { 34 | 35 | lock.wait() 36 | defer { lock.signal() } 37 | _value -= 1 38 | return _value 39 | } 40 | 41 | public func incrementAndGet() -> Int { 42 | 43 | lock.wait() 44 | defer { lock.signal() } 45 | _value += 1 46 | return _value 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ScClient/Utils/ClientUtils.swift: -------------------------------------------------------------------------------- 1 | public class ClientUtils { 2 | 3 | public static func getAuthToken(message : Any?) -> String? { 4 | if let items = message as? [String : Any] { 5 | if let data = items["data"] as? [String : Any] { 6 | return data["token"] as? String 7 | } 8 | } 9 | return nil 10 | } 11 | 12 | public static func getIsAuthenticated(message : Any?) -> Bool? { 13 | 14 | if let items = message as? [String : Any] { 15 | if let data = items["data"] as? [String : Any] { 16 | return data["isAuthenticated"] as? Bool 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ScClient/Utils/Miscellaneous.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class JSONConverter { 4 | 5 | public static func deserializeString(message : String) -> [String : Any]? { 6 | let jsonObject = try? JSONSerialization.jsonObject(with: message.data(using: .utf8)!, options: []) 7 | return jsonObject as? [String : Any] 8 | } 9 | 10 | public static func deserializeData(data : Data) -> [String : Any]? { 11 | let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) 12 | return jsonObject as? [String : Any] 13 | } 14 | 15 | public static func serializeObject(object : Any) -> String? { 16 | let message = try? JSONSerialization.data(withJSONObject: object, options: []) 17 | return String(data: message!, encoding: .utf8) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ScClient/client.swift: -------------------------------------------------------------------------------- 1 | import Starscream 2 | import Foundation 3 | 4 | 5 | public class ScClient : Listener, WebSocketDelegate { 6 | 7 | var authToken : String? 8 | var url : String? 9 | var socket : WebSocket 10 | var counter : AtomicInteger 11 | 12 | var onConnect : ((ScClient)-> Void)? 13 | var onConnectError : ((ScClient, Error?)-> Void)? 14 | var onDisconnect : ((ScClient, Error?)-> Void)? 15 | var onSetAuthentication : ((ScClient, String?)-> Void)? 16 | var onAuthentication : ((ScClient, Bool?)-> Void)? 17 | 18 | public func setAuthToken(token : String) { 19 | self.authToken = token 20 | } 21 | 22 | public func getAuthToken () -> String?{ 23 | return self.authToken 24 | } 25 | 26 | public func setBasicListener(onConnect : ((ScClient)-> Void)?, onConnectError : ((ScClient, Error?)-> Void)?, onDisconnect : ((ScClient, Error?)-> Void)?) { 27 | self.onConnect = onConnect 28 | self.onDisconnect = onDisconnect 29 | self.onConnectError = onConnectError 30 | } 31 | 32 | public func setAuthenticationListener (onSetAuthentication : ((ScClient, String?)-> Void)?, onAuthentication : ((ScClient, Bool?)-> Void)?) { 33 | self.onSetAuthentication = onSetAuthentication 34 | self.onAuthentication = onAuthentication 35 | } 36 | 37 | public func websocketDidConnect(socket: WebSocketClient) { 38 | counter.value = 0 39 | self.sendHandShake() 40 | onConnect?(self) 41 | } 42 | 43 | public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { 44 | onDisconnect?(self, error) 45 | } 46 | 47 | public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { 48 | if (text == "") { 49 | socket.write(string: "") 50 | } else { 51 | if let messageObject = JSONConverter.deserializeString(message: text) { 52 | if let (data, rid, cid, eventName, error) = Parser.getMessageDetails(myMessage: messageObject) { 53 | 54 | let parseResult = Parser.parse(rid: rid, cid: cid, event: eventName) 55 | 56 | switch parseResult { 57 | 58 | case .isAuthenticated: 59 | let isAuthenticated = ClientUtils.getIsAuthenticated(message: messageObject) 60 | onAuthentication?(self, isAuthenticated) 61 | case .publish: 62 | if let channel = Model.getChannelObject(data: data as AnyObject) { 63 | handleOnListener(eventName: channel.channel, data: channel.data as AnyObject) 64 | } 65 | case .removeToken: 66 | self.authToken = nil 67 | case .setToken: 68 | authToken = ClientUtils.getAuthToken(message: messageObject) 69 | self.onSetAuthentication?(self, authToken) 70 | case .ackReceive: 71 | 72 | handleEmitAck(id: rid!, error: error as AnyObject, data: data as AnyObject) 73 | case .event: 74 | if hasEventAck(eventName: eventName!) { 75 | handleOnAckListener(eventName: eventName!, data: data as AnyObject, ack: self.ack(cid: cid!)) 76 | } else { 77 | handleOnListener(eventName: eventName!, data: data as AnyObject) 78 | } 79 | 80 | } 81 | 82 | } 83 | } 84 | } 85 | } 86 | 87 | public func websocketDidReceiveData(socket: WebSocketClient, data: Data) { 88 | print("Received data: \(data.count)") 89 | } 90 | 91 | public init(url : String, authToken : String? = nil) { 92 | self.counter = AtomicInteger() 93 | self.authToken = authToken 94 | self.socket = WebSocket(url: URL(string: url)!) 95 | super.init() 96 | socket.delegate = self 97 | } 98 | 99 | public init(urlRequest : URLRequest, authToken : String? = nil, protocols : [String]? = nil) { 100 | self.counter = AtomicInteger() 101 | self.authToken = authToken 102 | self.socket = WebSocket(request: urlRequest, protocols : protocols) 103 | super.init() 104 | socket.delegate = self 105 | } 106 | 107 | public func connect() { 108 | socket.connect() 109 | } 110 | 111 | public func isConnected() -> Bool{ 112 | return socket.isConnected; 113 | } 114 | 115 | public func setBackgroundQueue(queueName : String) { 116 | socket.callbackQueue = DispatchQueue(label: queueName) 117 | } 118 | 119 | private func sendHandShake() { 120 | let handshake = Model.getHandshakeObject(authToken: self.authToken, messageId: counter.incrementAndGet()) 121 | socket.write(string: handshake.toJSONString()!) 122 | } 123 | 124 | private func ack(cid : Int) -> (AnyObject?, AnyObject?) -> Void { 125 | return { 126 | (error : AnyObject?, data : AnyObject?) in 127 | let ackObject = Model.getReceiveEventObject(data: data, error: error, messageId: cid) 128 | self.socket.write(string: ackObject.toJSONString()!) 129 | } 130 | } 131 | 132 | public func emit (eventName : String, data : AnyObject?) { 133 | let emitObject = Model.getEmitEventObject(eventName: eventName, data: data, messageId: counter.incrementAndGet()) 134 | self.socket.write(string : emitObject.toJSONString()!) 135 | } 136 | 137 | public func emitAck (eventName : String, data : AnyObject?, ack : @escaping (String, AnyObject?, AnyObject?)-> Void) { 138 | let id = counter.incrementAndGet() 139 | let emitObject = Model.getEmitEventObject(eventName: eventName, data: data, messageId: id) 140 | putEmitAck(id: id, eventName: eventName, ack: ack) 141 | self.socket.write(string : emitObject.toJSONString()!) 142 | } 143 | 144 | public func subscribe(channelName : String, token : String? = nil) { 145 | let subscribeObject = Model.getSubscribeEventObject(channelName: channelName, messageId: counter.incrementAndGet(), token : token) 146 | self.socket.write(string : subscribeObject.toJSONString()!) 147 | } 148 | 149 | public func subscribeAck(channelName : String, token : String? = nil, ack : @escaping (String, AnyObject?, AnyObject?)-> Void) { 150 | let id = counter.incrementAndGet() 151 | let subscribeObject = Model.getSubscribeEventObject(channelName: channelName, messageId: id, token : token) 152 | putEmitAck(id: id, eventName: channelName, ack: ack) 153 | self.socket.write(string : subscribeObject.toJSONString()!) 154 | } 155 | 156 | public func unsubscribe(channelName : String) { 157 | let unsubscribeObject = Model.getUnsubscribeEventObject(channelName: channelName, messageId: counter.incrementAndGet()) 158 | self.socket.write(string : unsubscribeObject.toJSONString()!) 159 | } 160 | 161 | public func unsubscribeAck(channelName : String, ack : @escaping (String, AnyObject?, AnyObject?)-> Void) { 162 | let id = counter.incrementAndGet() 163 | let unsubscribeObject = Model.getUnsubscribeEventObject(channelName: channelName, messageId: id) 164 | putEmitAck(id: id, eventName: channelName, ack: ack) 165 | self.socket.write(string : unsubscribeObject.toJSONString()!) 166 | } 167 | 168 | public func publish(channelName : String, data : AnyObject?) { 169 | let publishObject = Model.getPublishEventObject(channelName: channelName, data: data, messageId: counter.incrementAndGet()) 170 | self.socket.write(string : publishObject.toJSONString()!) 171 | } 172 | 173 | public func publishAck(channelName : String, data : AnyObject?, ack : @escaping (String, AnyObject?, AnyObject?)-> Void) { 174 | let id = counter.incrementAndGet() 175 | let publishObject = Model.getPublishEventObject(channelName: channelName, data: data, messageId: id) 176 | putEmitAck(id: id, eventName: channelName, ack: ack) 177 | self.socket.write(string : publishObject.toJSONString()!) 178 | } 179 | 180 | public func onChannel(channelName : String, ack : @escaping (String, AnyObject?) -> Void) { 181 | putOnListener(eventName: channelName, onListener: ack) 182 | } 183 | 184 | public func on(eventName : String, ack : @escaping (String, AnyObject?) -> Void) { 185 | putOnListener(eventName: eventName, onListener: ack) 186 | } 187 | 188 | public func onAck(eventName : String, ack : @escaping (String, AnyObject?, (AnyObject?, AnyObject?) -> Void) -> Void) { 189 | putOnAckListener(eventName: eventName, onAckListener: ack) 190 | } 191 | 192 | public func disconnect() { 193 | socket.disconnect() 194 | } 195 | 196 | public func disableSSLVerification(value : Bool) { 197 | socket.disableSSLCertValidation = value 198 | } 199 | 200 | /** 201 | Uses the .cer files in your app's bundle 202 | */ 203 | public func useSSLCertificate() { 204 | socket.security = SSLSecurity() 205 | } 206 | 207 | /** 208 | You load either a Data blob of your certificate or you can use a SecKeyRef if you have a public key you want to use. 209 | - Parameters: 210 | - data: Data blob of your certificate. 211 | - usePublicKeys: The usePublicKeys bool is whether to use the certificates for validation or the public keys. 212 | */ 213 | public func loadSSLCertificateFromData(data : Data, usePublicKeys : Bool = false) { 214 | socket.security = SSLSecurity(certs: [SSLCert(data: data)], usePublicKeys: usePublicKeys) 215 | } 216 | 217 | } 218 | 219 | -------------------------------------------------------------------------------- /Tests/Integration/EmitReceiveTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sachin Shinde on 09/02/20. 6 | // 7 | 8 | import XCTest 9 | @testable import Quick 10 | @testable import Nimble 11 | 12 | class EmitTest: QuickSpec { 13 | override func spec() { 14 | it("is friendly") { 15 | expect(true).to(beTruthy()) 16 | } 17 | 18 | it("is smart") { 19 | expect(true).to(beTruthy()) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/Integration/PubSubTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sachin Shinde on 09/02/20. 6 | // 7 | 8 | import XCTest 9 | @testable import Quick 10 | @testable import Nimble 11 | @testable import ScClient 12 | 13 | class PublishTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("The PUB-SUB mechanism") { 17 | 18 | beforeSuite { 19 | print("Pub sub mech") 20 | } 21 | describe("The channel", closure: { 22 | it("should be created successfully") { 23 | 24 | } 25 | 26 | print("The channel") 27 | 28 | describe("The publisher") { 29 | 30 | it("is able to publish message") { 31 | expect(true).to(beTruthy()) 32 | } 33 | 34 | it("is able to publish message and receive acknowledgement") { 35 | expect(true).to(beTruthy()) 36 | } 37 | print("The publisher") 38 | } 39 | 40 | describe("The subscriber") { 41 | 42 | it("is able to receive message") { 43 | expect(true).to(beTruthy()) 44 | } 45 | 46 | it("is able to publish message and receive acknowledgement") { 47 | expect(true).to(beTruthy()) 48 | } 49 | print("The subscriber") 50 | } 51 | }) 52 | 53 | afterSuite { 54 | 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/Integration/SetUp/ClientBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sachin Shinde on 09/02/20. 6 | // 7 | 8 | import XCTest 9 | @testable import ScClient 10 | 11 | class ScClientBuilder { 12 | 13 | public static func build(clientConfig: ClientConfig) -> ScClient { 14 | return ScClient(url: clientConfig.getUrl()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/Integration/SetUp/ClientConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sachin Shinde on 09/02/20. 6 | // 7 | 8 | import Foundation 9 | 10 | class ClientConfig { 11 | 12 | var host : String! 13 | var port : Int16! 14 | 15 | internal init(host: String?, port: Int16?) { 16 | self.host = host 17 | self.port = port 18 | } 19 | 20 | init() { 21 | self.host = "localhost" 22 | self.port = 8000 23 | } 24 | 25 | func getUrl() -> String { 26 | return "http://\(host!):\(port!)/socketcluster/" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Integration/SetUp/ClientPool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sachin Shinde on 11/02/20. 6 | // 7 | 8 | import Foundation 9 | @testable import ScClient 10 | 11 | class ClientPool { 12 | 13 | var clients : [String : ScClient] 14 | 15 | init() { 16 | clients = [String : ScClient]() 17 | } 18 | 19 | public static func create(name: String, clientConfig: ClientConfig) -> ClientPool { 20 | let clientPool = ClientPool() 21 | return clientPool.add(name: name, clientConfig: clientConfig) 22 | } 23 | 24 | public func add(name: String, clientConfig: ClientConfig) -> ClientPool { 25 | let scclient = ScClientBuilder.build(clientConfig: clientConfig) 26 | clients[name] = scclient 27 | return self 28 | } 29 | 30 | public func build() -> [String : ScClient] { 31 | return self.clients 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/Integration/SetUp/ClientPoolExecutor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Sachin Shinde on 11/02/20. 6 | // 7 | 8 | import Foundation 9 | import ScClient 10 | import RxSwift 11 | 12 | class ClientPoolExecutor { 13 | 14 | var noOfClients = 1 15 | var clientConfig: ClientConfig 16 | var queue = DispatchQueue(label: "com.clientPoolExecutor", qos: .default) 17 | 18 | init(noOfClients: Int32, clientConfig: ClientConfig, queue: DispatchQueue) { 19 | self.noOfClients = Int(noOfClients) 20 | self.clientConfig = clientConfig 21 | self.queue = queue 22 | } 23 | 24 | init(clientConfig: ClientConfig, queue: DispatchQueue) { 25 | self.clientConfig = clientConfig 26 | self.queue = queue 27 | } 28 | 29 | init(clientConfig: ClientConfig) { 30 | self.clientConfig = clientConfig 31 | } 32 | 33 | func ready() -> ReplaySubject { 34 | let pool: ClientPool = ClientPool.create(name: "client 1", clientConfig: self.clientConfig) 35 | for i in 2...self.noOfClients { 36 | let clientName = "client \(i)" 37 | pool.add(name: clientName, clientConfig: self.clientConfig) 38 | } 39 | let clientPublisher: ReplaySubject = ReplaySubject.create(bufferSize: self.noOfClients) 40 | let clients : [String : ScClient] = pool.build() 41 | for client in clients.values { 42 | client.setAuthenticationListener(onSetAuthentication: nil, onAuthentication: { 43 | (client :ScClient, isAuthenticated : Bool?) in 44 | clientPublisher.onNext(client) 45 | }) 46 | client.setBasicListener(onConnect: nil, onConnectError: { (client: ScClient, error: Error?) in 47 | clientPublisher.onError(error!) 48 | }, onDisconnect: nil) 49 | queue.async { 50 | client.setBackgroundQueue(queueName: "com.backgroundQueue") 51 | client.connect() 52 | } 53 | } 54 | return clientPublisher 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/ScClientTestSuite: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ScClientTests 3 | 4 | XCTMain([ 5 | // testCase(ScClient.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/Unit/ClientUtilsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientUtilsTest.swift 3 | // ScClientTests 4 | // 5 | // Created by admin on 12/01/2018. 6 | // 7 | 8 | import XCTest 9 | @testable import ScClient 10 | 11 | class ClientUtilsTest: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | 19 | func testShouldReturnAuthToken() { 20 | let event = "{\"event\":\"#setAuthToken\",\"data\": {\"token\":\"234234\"},\"cid\": 2}" 21 | let jsonObject = JSONConverter.deserializeString(message: event) 22 | let actualtoken = ClientUtils.getAuthToken(message: jsonObject) 23 | XCTAssertEqual("234234", actualtoken) 24 | 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testShouldReturnAuthenticationFlag() { 30 | let event = "{\"rid\":1,\"data\":{\"id\":\"nhI9Ry88h_XpLHwEAAAF\",\"isAuthenticated\":false,\"pingTimeout\":20000}}" 31 | let jsonObject = JSONConverter.deserializeString(message: event) 32 | let actualAuthenticationFlag = ClientUtils.getIsAuthenticated(message: jsonObject) 33 | XCTAssertEqual(false, actualAuthenticationFlag) 34 | 35 | } 36 | 37 | override func tearDown() { 38 | // Put teardown code here. This method is called after the invocation of each test method in the class. 39 | super.tearDown() 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Tests/Unit/MiscellaneousTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ScClient 3 | 4 | class MiscellaneousTest: XCTestCase { 5 | 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | } 11 | 12 | func skipTestShouldSerializeData() { 13 | let emitEvent = Model.getEmitEventObject(eventName: "chat", data: "My Sample Data" as AnyObject, messageId: 2) 14 | let expectedData = "{\"data\":\"My Sample Data\",\"event\":\"chat\",\"cid\":2}" 15 | XCTAssertEqual(expectedData, emitEvent.toJSONString()) 16 | } 17 | 18 | func testShouldDeserializeData() { 19 | let jsonString = "{\"cid\":2,\"event\":\"chat\",\"data\":\"My Sample Data\"}" 20 | if let emitEvent = EmitEvent.deserialize(from: jsonString) { 21 | XCTAssertEqual(emitEvent.cid, 2) 22 | XCTAssertEqual(emitEvent.data as? String, "My Sample Data") 23 | XCTAssertEqual(emitEvent.event, "chat") 24 | } 25 | } 26 | 27 | override func tearDown() { 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Tests/Unit/ParserTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | @testable import ScClient 4 | 5 | class ParserTest: XCTestCase { 6 | 7 | func testShouldReturnPublish() { 8 | let expectedParseResult = MessageType.publish 9 | let actaulParseResult = Parser.parse(rid: 1, cid: 1, event: "#publish") 10 | XCTAssertEqual(expectedParseResult, actaulParseResult) 11 | 12 | } 13 | 14 | func testShouldReturnRemoveAuthToken() { 15 | let expectedParseResult = MessageType.removeToken 16 | let actaulParseResult = Parser.parse(rid: 1,cid: 0,event: "#removeAuthToken") 17 | XCTAssertEqual(expectedParseResult, actaulParseResult) 18 | 19 | } 20 | 21 | func testShouldReturnSetAuthToken() { 22 | let expectedParseResult = MessageType.setToken 23 | let actaulParseResult = Parser.parse(rid: 1,cid: 0,event: "#setAuthToken") 24 | XCTAssertEqual(expectedParseResult, actaulParseResult) 25 | } 26 | 27 | func testShouldReturnEvent() { 28 | let expectedParseResult = MessageType.event 29 | let actaulParseResult = Parser.parse(rid: 1,cid: 0, event: "chat") 30 | XCTAssertEqual(expectedParseResult, actaulParseResult) 31 | } 32 | 33 | func testShouldReturnIsAuthenticated() { 34 | let expectedParseResult = MessageType.isAuthenticated 35 | let actaulParseResult = Parser.parse(rid: 1,cid: 0,event: nil) 36 | XCTAssertEqual(expectedParseResult, actaulParseResult) 37 | } 38 | 39 | func testShouldReturnAckReceive() { 40 | let expectedParseResult = MessageType.ackReceive 41 | let actaulParseResult = Parser.parse(rid: 12,cid: 0, event: nil) 42 | XCTAssertEqual(expectedParseResult, actaulParseResult) 43 | } 44 | 45 | func testShouldReturnMessageDetails() { 46 | let message = "{\"event\":\"#removeAuthToken\",\"data\":\"This is a sample data\",\"cid\":1, \"rid\":2, \"error\":\"This is a sample error\"}" 47 | let messageObject = JSONConverter.deserializeString(message: message) 48 | 49 | if let result = Parser.getMessageDetails(myMessage: messageObject as Any) { 50 | XCTAssertEqual( "This is a sample data", result.data as! String) 51 | XCTAssertEqual( 2, result.rid) 52 | XCTAssertEqual( 1, result.cid) 53 | XCTAssertEqual( "#removeAuthToken", result.eventName) 54 | XCTAssertEqual( "This is a sample error", result.error as! String) 55 | } 56 | 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Tests/Unit/UtilsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilsTest.swift 3 | // ScClient 4 | // 5 | // Created by admin on 07/01/2018. 6 | // 7 | 8 | import XCTest 9 | @testable import ScClient 10 | 11 | class UtilsTest: XCTestCase { 12 | 13 | var counter : AtomicInteger! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | counter = AtomicInteger() 18 | } 19 | 20 | func testShouldIncrementCounterByOne() { 21 | XCTAssertEqual(counter.incrementAndGet(), 1); 22 | } 23 | 24 | func testShouldDecrementCounterByOne() { 25 | counter.value = 2 26 | XCTAssertEqual(counter.decrementAndGet(), 1); 27 | } 28 | 29 | override func tearDown() { 30 | counter = nil 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /commands.txt: -------------------------------------------------------------------------------- 1 | ### Swift Package Manager 2 | 3 | swift package init — type=library 4 | open -a Xcode Package.swift 5 | swift package resolve 6 | swift package build 7 | swift package test 8 | swift package generate-xcodeproj 9 | swift build 10 | git tag -a 1.0.3 -m "added support for setting background queue, set custom request headers and connection status" 11 | swift build clean 12 | swift build test 13 | 14 | 15 | ### Cocoa Pod 16 | 17 | sudo gem install cocoapods 18 | pod --version 19 | pod install 20 | pod setup 21 | pod update 22 | sudo rm -fr ~/.cocoapods/repos/master 23 | pod spec create —help 24 | pod spec create https://github.com/sacOO7/ScClient 25 | pod spec create ScClient 26 | open -a Xcode ScClient.podspec 27 | // Register trunk here with proper description 28 | pod spec lint ScClient.podspec --allow-warnings 29 | pod trunk push ScClient.podspec --allow-warnings 30 | 31 | --------------------------------------------------------------------------------