├── .github └── workflows │ ├── release.yml │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CHANGELOG.md ├── LICENSE.md ├── NWWebSocket.podspec ├── NWWebSocket.xcodeproj ├── NWWebSocketTests_Info.plist ├── NWWebSocket_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── NWWebSocket-Package.xcscheme ├── Package.swift ├── README.md ├── Scripts └── generate-api-docs.sh ├── Sources └── NWWebSocket │ ├── Extension │ └── NWConnection+Extension.swift │ ├── Model │ └── Client │ │ └── NWWebSocket.swift │ └── Protocol │ └── WebSocketConnection.swift ├── Tests ├── LinuxMain.swift └── NWWebSocketTests │ ├── NWWebSocketTests.swift │ ├── Server │ ├── NWServerConnection.swift │ └── NWWebSocketServer.swift │ └── XCTestManifests.swift └── docs ├── Classes.html ├── Classes └── NWWebSocket.html ├── Protocols.html ├── Protocols ├── WebSocketConnection.html └── WebSocketConnectionDelegate.html ├── badge.svg ├── css ├── highlight.css └── jazzy.css ├── docsets ├── NWWebSocket.docset │ └── Contents │ │ ├── Info.plist │ │ └── Resources │ │ ├── Documents │ │ ├── Classes.html │ │ ├── Classes │ │ │ └── NWWebSocket.html │ │ ├── Protocols.html │ │ ├── Protocols │ │ │ ├── WebSocketConnection.html │ │ │ └── WebSocketConnectionDelegate.html │ │ ├── badge.svg │ │ ├── css │ │ │ ├── highlight.css │ │ │ └── jazzy.css │ │ ├── img │ │ │ ├── carat.png │ │ │ ├── dash.png │ │ │ ├── gh.png │ │ │ └── spinner.gif │ │ ├── index.html │ │ ├── js │ │ │ ├── jazzy.js │ │ │ ├── jazzy.search.js │ │ │ ├── jquery.min.js │ │ │ ├── lunr.min.js │ │ │ └── typeahead.jquery.js │ │ ├── search.json │ │ └── undocumented.json │ │ └── docSet.dsidx └── NWWebSocket.tgz ├── img ├── carat.png ├── dash.png ├── gh.png └── spinner.gif ├── index.html ├── js ├── jazzy.js ├── jazzy.search.js ├── jquery.min.js ├── lunr.min.js └── typeahead.jquery.js ├── search.json └── undocumented.json /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | 5 | jobs: 6 | check-release-tag: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 13 | - uses: actions/setup-node@v3 14 | - name: Prepare tag 15 | id: prepare_tag 16 | continue-on-error: true 17 | run: | 18 | npm install --location=global podspec-bump 19 | export TAG=$(podspec-bump --dump-version -p NWWebSocket.podspec) 20 | echo "TAG=$TAG" >> $GITHUB_ENV 21 | export CHECK_TAG=$(git tag | grep $TAG) 22 | if [[ $CHECK_TAG ]]; then 23 | echo "Skipping because release tag already exists" 24 | exit 1 25 | fi 26 | - name: Output 27 | id: release_output 28 | if: ${{ steps.prepare_tag.outcome == 'success' }} 29 | run: | 30 | echo "::set-output name=tag::${{ env.TAG }}" 31 | outputs: 32 | tag: ${{ steps.release_output.outputs.tag }} 33 | 34 | build: 35 | runs-on: macos-latest 36 | needs: check-release-tag 37 | if: ${{ needs.check-release-tag.outputs.tag }} 38 | steps: 39 | - uses: actions/checkout@v2 40 | - name: Build 41 | run: swift build -v 42 | - name: Run tests 43 | run: swift test -v 44 | outputs: 45 | tag: ${{ needs.check-release-tag.outputs.tag }} 46 | 47 | publish-cocoapods: 48 | runs-on: ubuntu-latest 49 | needs: build 50 | if: ${{ needs.build.outputs.tag }} 51 | steps: 52 | - uses: actions/checkout@v2 53 | - uses: ruby/setup-ruby@v1 54 | with: 55 | ruby-version: '3.2.2' 56 | - run: | 57 | gem install cocoapods 58 | 59 | git config user.email "pusher-ci@pusher.com" 60 | git config user.name "Pusher CI" 61 | 62 | git tag -a ${{ needs.build.outputs.tag }} -m "${{ needs.build.outputs.tag }}" 63 | git push origin ${{ needs.build.outputs.tag }} 64 | 65 | pod trunk push NWWebSocket.podspec 66 | env: 67 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.PUSHER_CI_COCOAPODS_TOKEN }} 68 | outputs: 69 | release_version: ${{ needs.build.outputs.tag }} 70 | 71 | create-github-release: 72 | runs-on: ubuntu-latest 73 | needs: publish-cocoapods 74 | if: ${{ needs.publish-cocoapods.outputs.release_version }} 75 | steps: 76 | - uses: actions/checkout@v2 77 | - name: Prepare tag 78 | run: | 79 | export TAG=${{ needs.publish-cocoapods.outputs.release_version }} 80 | echo "TAG=$TAG" >> $GITHUB_ENV 81 | - name: Setup git 82 | run: | 83 | git config user.email "pusher-ci@pusher.com" 84 | git config user.name "Pusher CI" 85 | - name: Create Release 86 | uses: actions/create-release@v1 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | with: 90 | tag_name: ${{ env.TAG }} 91 | release_name: ${{ env.TAG }} 92 | draft: false 93 | prerelease: false 94 | 95 | 96 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased](https://github.com/pusher/NWWebSocket/compare/0.5.4...HEAD) 8 | 9 | ## [0.5.4](https://github.com/pusher/NWWebSocket/compare/0.5.3...0.5.4) - 2023-12-15 10 | 11 | ### Fixed 12 | 13 | - Fix reconnection loop [#44] 14 | 15 | ## [0.5.3](https://github.com/pusher/NWWebSocket/compare/0.5.2...0.5.3) - 2023-04-04 16 | 17 | ### Fixed 18 | 19 | - Prevent memory leak when disconnecting and reconnecting. 20 | 21 | ## [0.5.2](https://github.com/pusher/NWWebSocket/compare/0.5.1...0.5.2) - 2021-03-11 22 | 23 | ### Fixed 24 | 25 | - Resolved an issue preventing App Store submission when integrating NWWebSocket using certain dependency managers. 26 | 27 | ## [0.5.1](https://github.com/pusher/NWWebSocket/compare/0.5.0...0.5.1) - 2020-12-15 28 | 29 | ### Fixed 30 | 31 | - Resolved a race condition that could prevent a manual reconnection attempt in certain circumstances. 32 | 33 | ## [0.5.0](https://github.com/pusher/NWWebSocket/compare/0.4.0...0.5.0) - 2020-11-20 34 | 35 | ### Added 36 | 37 | - Connection state reporting and automatic migration when a better network path becomes available. 38 | 39 | ### Changed 40 | 41 | - Improved Apple Quick Help documentation comments coverage. 42 | - Error-reporting improvements (passes the `NWError` directly via the delegate callback). 43 | 44 | ## [0.4.0](https://github.com/pusher/NWWebSocket/compare/0.3.0...0.4.0) - 2020-10-27 45 | 46 | ### Added 47 | 48 | - watchOS support (6.0 and above). 49 | - Additions to the README to help new users of `NWWebSocket` library. 50 | 51 | ## [0.3.0](https://github.com/pusher/NWWebSocket/compare/0.2.1...0.3.0) - 2020-10-16 52 | 53 | ### Added 54 | 55 | - [Cocoapods](https://cocoapods.org/) support. 56 | 57 | ## [0.2.1](https://github.com/pusher/NWWebSocket/compare/0.2.0...0.2.1) - 2020-10-16 58 | 59 | ### Added 60 | 61 | - This CHANGELOG file. 62 | 63 | ### Changed 64 | 65 | - `NWWebSocket` class (and some methods) are now defined as `open` (previously they were `public`). 66 | 67 | ## [0.2.0](https://github.com/pusher/NWWebSocket/compare/0.1.0...0.2.0) - 2020-10-15 68 | 69 | ### Added 70 | 71 | - [Carthage](https://github.com/Carthage/Carthage) support. 72 | 73 | ## [0.1.0](https://github.com/pusher/NWWebSocket/compare/dcab0c4dc704ffc3510adc3a2aa8853be49aa9f6...0.1.0) - 2020-10-15 74 | 75 | ### Added 76 | 77 | - Initial version of `NWWebSocket`. 78 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pusher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NWWebSocket.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'NWWebSocket' 3 | s.version = '0.5.4' 4 | s.summary = 'A WebSocket client written in Swift, using the Network framework from Apple' 5 | s.homepage = 'https://github.com/pusher/NWWebSocket' 6 | s.license = 'MIT' 7 | s.author = { "Pusher Limited" => "support@pusher.com" } 8 | s.source = { git: "https://github.com/pusher/NWWebSocket.git", tag: s.version.to_s } 9 | s.social_media_url = 'https://twitter.com/pusher' 10 | 11 | s.swift_version = '5.1' 12 | s.requires_arc = true 13 | s.source_files = ['Sources/**/*.swift'] 14 | 15 | s.ios.deployment_target = '13.0' 16 | s.osx.deployment_target = '10.15' 17 | s.tvos.deployment_target = '13.0' 18 | s.watchos.deployment_target = '6.0' 19 | end 20 | -------------------------------------------------------------------------------- /NWWebSocket.xcodeproj/NWWebSocketTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NWWebSocket.xcodeproj/NWWebSocket_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NWWebSocket.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /NWWebSocket.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NWWebSocket.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /NWWebSocket.xcodeproj/xcshareddata/xcschemes/NWWebSocket-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "NWWebSocket", 7 | platforms: [.iOS("13.0"), 8 | .macOS("10.15"), 9 | .tvOS("13.0"), 10 | .watchOS("6.0")], 11 | products: [ 12 | .library( 13 | name: "NWWebSocket", 14 | targets: ["NWWebSocket"]), 15 | ], 16 | dependencies: [], 17 | targets: [ 18 | .target( 19 | name: "NWWebSocket", 20 | dependencies: []), 21 | .testTarget( 22 | name: "NWWebSocketTests", 23 | dependencies: ["NWWebSocket"]), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NWWebSocket 2 | 3 | ![Build Status](https://github.com/pusher/NWWebSocket/workflows/CI/badge.svg) 4 | [![Latest Release](https://img.shields.io/github/v/release/pusher/NWWebSocket)](https://github.com/pusher/NWWebSocket/releases) 5 | [![API Docs](https://img.shields.io/badge/Docs-here!-lightgrey)](https://pusher.github.io/NWWebSocket/) 6 | [![Supported Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpusher%2FNWWebSocket%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pusher/NWWebSocket) 7 | [![Swift Versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpusher%2FNWWebSocket%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pusher/NWWebSocket) 8 | [![Cocoapods Compatible](https://img.shields.io/cocoapods/v/NWWebSocket.svg)](https://cocoapods.org/pods/NWWebSocket) 9 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 10 | [![Twitter](https://img.shields.io/badge/twitter-@Pusher-blue.svg?style=flat)](http://twitter.com/Pusher) 11 | [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/pusher/NWWebSocket/master/LICENSE.md) 12 | 13 | A WebSocket client written in Swift, using the Network framework from Apple. 14 | 15 | - [Supported platforms](#supported-platforms) 16 | - [Installation](#installation) 17 | - [Usage](#usage) 18 | - [Documentation](#documentation) 19 | - [Reporting bugs and requesting features](#reporting-bugs-and-requesting-features) 20 | - [Credits](#credits) 21 | - [License](#license) 22 | 23 | ## Supported platforms 24 | - Swift 5.1 and above 25 | - Xcode 11.0 and above 26 | 27 | ### Deployment targets 28 | - iOS 13.0 and above 29 | - macOS 10.15 and above 30 | - tvOS 13.0 and above 31 | 32 | ## Installation 33 | 34 | ### CocoaPods 35 | 36 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. 37 | 38 | If you don't already have the Cocoapods gem installed, run the following command: 39 | 40 | ```bash 41 | $ gem install cocoapods 42 | ``` 43 | 44 | To integrate NWWebSocket into your Xcode project using CocoaPods, specify it in your `Podfile`: 45 | 46 | ```ruby 47 | source 'https://github.com/CocoaPods/Specs.git' 48 | platform :ios, '14.0' 49 | use_frameworks! 50 | 51 | pod 'NWWebSocket', '~> 0.5.4' 52 | ``` 53 | 54 | Then, run the following command: 55 | 56 | ```bash 57 | $ pod install 58 | ``` 59 | 60 | If you find that you're not having the most recent version installed when you run `pod install` then try running: 61 | 62 | ```bash 63 | $ pod cache clean 64 | $ pod repo update NWWebSocket 65 | $ pod install 66 | ``` 67 | 68 | Also you'll need to make sure that you've not got the version of NWWebSocket locked to an old version in your `Podfile.lock` file. 69 | 70 | ### Swift Package Manager 71 | 72 | To integrate the library into your project using [Swift Package Manager](https://swift.org/package-manager/), you can add the library as a dependency in Xcode – see the [docs](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app). The package repository URL is: 73 | 74 | ```bash 75 | https://github.com/pusher/NWWebSocket.git 76 | ``` 77 | 78 | Alternatively, you can add the library as a dependency in your `Package.swift` file. For example: 79 | 80 | ```swift 81 | // swift-tools-version:5.1 82 | import PackageDescription 83 | 84 | let package = Package( 85 | name: "YourPackage", 86 | products: [ 87 | .library( 88 | name: "YourPackage", 89 | targets: ["YourPackage"]), 90 | ], 91 | dependencies: [ 92 | .package(url: "https://github.com/pusher/NWWebSocket.git", 93 | .upToNextMajor(from: "0.5.4")), 94 | ], 95 | targets: [ 96 | .target( 97 | name: "YourPackage", 98 | dependencies: ["NWWebSocket"]), 99 | ] 100 | ) 101 | ``` 102 | 103 | You will then need to include both `import Network` and `import NWWebSocket` statements in any source files where you wish to use the library. 104 | 105 | ## Usage 106 | 107 | This section describes how to configure and use NWWebSocket to manage a WebSocket connection. 108 | 109 | ### Connection and disconnection 110 | 111 | Connection and disconnection is straightforward. Connecting to a WebSocket is manual by default, setting `connectAutomatically` to `true` makes connection automatic. 112 | 113 | #### Manual connection 114 | 115 | ```swift 116 | let socketURL = URL(string: "wss://somewebsockethost.com") 117 | let socket = NWWebSocket(url: socketURL) 118 | socket.delegate = self 119 | socket.connect() 120 | 121 | // Use the WebSocket… 122 | 123 | socket.disconnect() 124 | ``` 125 | 126 | #### Automatic connection 127 | 128 | ```swift 129 | let socketURL = URL(string: "wss://somewebsockethost.com") 130 | let socket = NWWebSocket(url: socketURL, connectAutomatically: true) 131 | socket.delegate = self 132 | 133 | // Use the WebSocket… 134 | 135 | socket.disconnect() 136 | ``` 137 | 138 | **NOTES:** 139 | 140 | - In the above code examples, `self` must conform to `WebSocketConnectionDelegate` (see [Receiving messages and connection updates](#receiving-messages-and-connection-updates)) 141 | 142 | ### Sending data 143 | 144 | UTF-8 encoded strings or binary data can be sent over the WebSocket connection. 145 | 146 | ```swift 147 | // Sending a `String` 148 | let message = "Hello, world!" 149 | socket.send(string: message) 150 | 151 | // Sending some binary data 152 | let data: [UInt8] = [123, 234] 153 | let messageData = Data(data) 154 | socket.send(data: messageData) 155 | ``` 156 | 157 | ### Receiving messages and connection updates 158 | 159 | String or data messages (as well as connection state updates) can be received by making a type you define conform to `WebSocketConnectionDelegate`. You can then respond to received messages or connection events accordingly. 160 | 161 | ```swift 162 | extension MyWebSocketConnectionManager: WebSocketConnectionDelegate { 163 | 164 | func webSocketDidConnect(connection: WebSocketConnection) { 165 | // Respond to a WebSocket connection event 166 | } 167 | 168 | func webSocketDidDisconnect(connection: WebSocketConnection, 169 | closeCode: NWProtocolWebSocket.CloseCode, reason: Data?) { 170 | // Respond to a WebSocket disconnection event 171 | } 172 | 173 | func webSocketViabilityDidChange(connection: WebSocketConnection, isViable: Bool) { 174 | // Respond to a WebSocket connection viability change event 175 | } 176 | 177 | func webSocketDidAttemptBetterPathMigration(result: Result) { 178 | // Respond to when a WebSocket connection migrates to a better network path 179 | // (e.g. A device moves from a cellular connection to a Wi-Fi connection) 180 | } 181 | 182 | func webSocketDidReceiveError(connection: WebSocketConnection, error: NWError) { 183 | // Respond to a WebSocket error event 184 | } 185 | 186 | func webSocketDidReceivePong(connection: WebSocketConnection) { 187 | // Respond to a WebSocket connection receiving a Pong from the peer 188 | } 189 | 190 | func webSocketDidReceiveMessage(connection: WebSocketConnection, string: String) { 191 | // Respond to a WebSocket connection receiving a `String` message 192 | } 193 | 194 | func webSocketDidReceiveMessage(connection: WebSocketConnection, data: Data) { 195 | // Respond to a WebSocket connection receiving a binary `Data` message 196 | } 197 | } 198 | ``` 199 | 200 | ### Ping and pong 201 | 202 | Triggering a Ping on an active WebSocket connection is a best practice method of telling the connected peer that the connection should be maintained. Pings can be triggered on-demand or periodically. 203 | 204 | ```swift 205 | 206 | // Trigger a Ping on demand 207 | socket.ping() 208 | 209 | // Trigger a Ping periodically 210 | // (This is useful when messages are infrequently sent across the connection to prevent a connection closure) 211 | socket.ping(interval: 30.0) 212 | ``` 213 | 214 | ## Documentation 215 | 216 | Full documentation of the library can be found in the [API docs](https://pusher.github.io/NWWebSocket/). 217 | 218 | ## Reporting bugs and requesting features 219 | - If you have found a bug or have a feature request, please open an issue 220 | - If you want to contribute, please submit a pull request (preferably with some tests 🙂 ) 221 | 222 | ## Credits 223 | 224 | NWWebSocket is owned and maintained by [Pusher](https://pusher.com). It was originally created by [Daniel Browne](https://github.com/danielrbrowne). 225 | 226 | It uses code from the following repositories: 227 | 228 | - [perpetual-learning](https://github.com/MichaelNeas/perpetual-learning/tree/master/ios-sockets) 229 | 230 | ## License 231 | 232 | NWWebSocket is released under the MIT license. See [LICENSE](https://github.com/pusher/NWWebSocket/blob/master/LICENSE.md) for details. 233 | -------------------------------------------------------------------------------- /Scripts/generate-api-docs.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | if ! which jazzy >/dev/null; then 6 | echo "Error: Jazzy not installed, see https://github.com/realm/Jazzy or run 'gem install jazzy' to install it" 7 | exit 1 8 | fi 9 | 10 | if ! which jq >/dev/null; then 11 | echo "Error: jq not installed, run 'brew install jq' to install it" 12 | exit 1 13 | fi 14 | 15 | read -p 'Enter release tag (without quotes): ' RELEASE_TAG 16 | 17 | AUTHOR_NAME="Pusher Limited" 18 | AUTHOR_URL="https://pusher.com" 19 | GITHUB_ORIGIN=$(git remote get-url origin) 20 | GITHUB_URL=${GITHUB_ORIGIN%".git"} 21 | MODULE_NAME=$(swift package dump-package | jq --raw-output '.name') 22 | 23 | echo "Generating public API docs from release tag $RELEASE_TAG" 24 | 25 | # The 'arch -x86_64' command is redundant on Intel Macs, and runs Jazzy under Rosetta 2 on Apple Silicon Macs for compatibility 26 | arch -x86_64 jazzy \ 27 | --module $MODULE_NAME \ 28 | --module_version $RELEASE_TAG \ 29 | --author $AUTHOR_NAME \ 30 | --author_url $AUTHOR_URL \ 31 | --github_url $GITHUB_URL \ 32 | --github-file-prefix $GITHUB_URL/tree/$RELEASE_TAG \ 33 | --swift-build-tool spm -------------------------------------------------------------------------------- /Sources/NWWebSocket/Extension/NWConnection+Extension.swift: -------------------------------------------------------------------------------- 1 | import Network 2 | 3 | fileprivate var _intentionalDisconnection: Bool = false 4 | 5 | internal extension NWConnection { 6 | 7 | var intentionalDisconnection: Bool { 8 | get { 9 | return _intentionalDisconnection 10 | } 11 | set { 12 | _intentionalDisconnection = newValue 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/NWWebSocket/Model/Client/NWWebSocket.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Network 3 | 4 | /// A WebSocket client that manages a socket connection. 5 | open class NWWebSocket: WebSocketConnection { 6 | 7 | // MARK: - Public properties 8 | 9 | /// The WebSocket connection delegate. 10 | public weak var delegate: WebSocketConnectionDelegate? 11 | 12 | /// The default `NWProtocolWebSocket.Options` for a WebSocket connection. 13 | /// 14 | /// These options specify that the connection automatically replies to Ping messages 15 | /// instead of delivering them to the `receiveMessage(data:context:)` method. 16 | public static var defaultOptions: NWProtocolWebSocket.Options { 17 | let options = NWProtocolWebSocket.Options() 18 | options.autoReplyPing = true 19 | 20 | return options 21 | } 22 | 23 | private let errorWhileWaitingLimit = 20 24 | 25 | // MARK: - Private properties 26 | 27 | private var connection: NWConnection? 28 | private let endpoint: NWEndpoint 29 | private let parameters: NWParameters 30 | private let connectionQueue: DispatchQueue 31 | private var pingTimer: Timer? 32 | private var disconnectionWorkItem: DispatchWorkItem? 33 | private var isMigratingConnection = false 34 | private var errorWhileWaitingCount = 0 35 | 36 | // MARK: - Initialization 37 | 38 | /// Creates a `NWWebSocket` instance which connects to a socket `url` with some configuration `options`. 39 | /// - Parameters: 40 | /// - request: The `URLRequest` containing the connection endpoint `URL`. 41 | /// - connectAutomatically: Determines if a connection should occur automatically on initialization. 42 | /// The default value is `false`. 43 | /// - options: The configuration options for the connection. The default value is `NWWebSocket.defaultOptions`. 44 | /// - connectionQueue: A `DispatchQueue` on which to deliver all connection events. The default value is `.main`. 45 | public convenience init(request: URLRequest, 46 | connectAutomatically: Bool = false, 47 | options: NWProtocolWebSocket.Options = NWWebSocket.defaultOptions, 48 | connectionQueue: DispatchQueue = .main) { 49 | 50 | self.init(url: request.url!, 51 | connectAutomatically: connectAutomatically, 52 | connectionQueue: connectionQueue) 53 | } 54 | 55 | /// Creates a `NWWebSocket` instance which connects a socket `url` with some configuration `options`. 56 | /// - Parameters: 57 | /// - url: The connection endpoint `URL`. 58 | /// - connectAutomatically: Determines if a connection should occur automatically on initialization. 59 | /// The default value is `false`. 60 | /// - options: The configuration options for the connection. The default value is `NWWebSocket.defaultOptions`. 61 | /// - connectionQueue: A `DispatchQueue` on which to deliver all connection events. The default value is `.main`. 62 | public init(url: URL, 63 | connectAutomatically: Bool = false, 64 | options: NWProtocolWebSocket.Options = NWWebSocket.defaultOptions, 65 | connectionQueue: DispatchQueue = .main) { 66 | 67 | endpoint = .url(url) 68 | 69 | if url.scheme == "ws" { 70 | parameters = NWParameters.tcp 71 | } else { 72 | parameters = NWParameters.tls 73 | } 74 | 75 | parameters.defaultProtocolStack.applicationProtocols.insert(options, at: 0) 76 | 77 | self.connectionQueue = connectionQueue 78 | 79 | if connectAutomatically { 80 | connect() 81 | } 82 | } 83 | 84 | deinit { 85 | connection?.intentionalDisconnection = true 86 | connection?.cancel() 87 | } 88 | 89 | // MARK: - WebSocketConnection conformance 90 | 91 | /// Connect to the WebSocket. 92 | open func connect() { 93 | if connection == nil { 94 | connection = NWConnection(to: endpoint, using: parameters) 95 | connection?.stateUpdateHandler = { [weak self] state in 96 | self?.stateDidChange(to: state) 97 | } 98 | connection?.betterPathUpdateHandler = { [weak self] isAvailable in 99 | self?.betterPath(isAvailable: isAvailable) 100 | } 101 | connection?.viabilityUpdateHandler = { [weak self] isViable in 102 | self?.viabilityDidChange(isViable: isViable) 103 | } 104 | listen() 105 | connection?.start(queue: connectionQueue) 106 | } else if connection?.state != .ready && !isMigratingConnection { 107 | connection?.start(queue: connectionQueue) 108 | } 109 | } 110 | 111 | /// Send a UTF-8 formatted `String` over the WebSocket. 112 | /// - Parameter string: The `String` that will be sent. 113 | open func send(string: String) { 114 | guard let data = string.data(using: .utf8) else { 115 | return 116 | } 117 | let metadata = NWProtocolWebSocket.Metadata(opcode: .text) 118 | let context = NWConnection.ContentContext(identifier: "textContext", 119 | metadata: [metadata]) 120 | 121 | send(data: data, context: context) 122 | } 123 | 124 | /// Send some `Data` over the WebSocket. 125 | /// - Parameter data: The `Data` that will be sent. 126 | open func send(data: Data) { 127 | let metadata = NWProtocolWebSocket.Metadata(opcode: .binary) 128 | let context = NWConnection.ContentContext(identifier: "binaryContext", 129 | metadata: [metadata]) 130 | 131 | send(data: data, context: context) 132 | } 133 | 134 | /// Start listening for messages over the WebSocket. 135 | public func listen() { 136 | connection?.receiveMessage { [weak self] (data, context, _, error) in 137 | guard let self = self else { 138 | return 139 | } 140 | 141 | if let data = data, !data.isEmpty, let context = context { 142 | self.receiveMessage(data: data, context: context) 143 | } 144 | 145 | if let error = error { 146 | self.reportErrorOrDisconnection(error) 147 | } else { 148 | self.listen() 149 | } 150 | } 151 | } 152 | 153 | /// Ping the WebSocket periodically. 154 | /// - Parameter interval: The `TimeInterval` (in seconds) with which to ping the server. 155 | open func ping(interval: TimeInterval) { 156 | pingTimer = .scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in 157 | guard let self = self else { 158 | return 159 | } 160 | 161 | self.ping() 162 | } 163 | pingTimer?.tolerance = 0.01 164 | } 165 | 166 | /// Ping the WebSocket once. 167 | open func ping() { 168 | let metadata = NWProtocolWebSocket.Metadata(opcode: .ping) 169 | metadata.setPongHandler(connectionQueue) { [weak self] error in 170 | guard let self = self else { 171 | return 172 | } 173 | 174 | if let error = error { 175 | self.reportErrorOrDisconnection(error) 176 | } else { 177 | self.delegate?.webSocketDidReceivePong(connection: self) 178 | } 179 | } 180 | let context = NWConnection.ContentContext(identifier: "pingContext", 181 | metadata: [metadata]) 182 | 183 | send(data: "ping".data(using: .utf8), context: context) 184 | } 185 | 186 | /// Disconnect from the WebSocket. 187 | /// - Parameter closeCode: The code to use when closing the WebSocket connection. 188 | open func disconnect(closeCode: NWProtocolWebSocket.CloseCode = .protocolCode(.normalClosure)) { 189 | connection?.intentionalDisconnection = true 190 | 191 | // Call `cancel()` directly for a `normalClosure` 192 | // (Otherwise send the custom closeCode as a message). 193 | if closeCode == .protocolCode(.normalClosure) { 194 | connection?.cancel() 195 | scheduleDisconnectionReporting(closeCode: closeCode, 196 | reason: nil) 197 | } else { 198 | let metadata = NWProtocolWebSocket.Metadata(opcode: .close) 199 | metadata.closeCode = closeCode 200 | let context = NWConnection.ContentContext(identifier: "closeContext", 201 | metadata: [metadata]) 202 | 203 | if connection?.state == .ready { 204 | // See implementation of `send(data:context:)` for `scheduleDisconnection(closeCode:, reason:)` 205 | send(data: nil, context: context) 206 | } else { 207 | scheduleDisconnectionReporting(closeCode: closeCode, reason: nil) 208 | } 209 | } 210 | } 211 | 212 | // MARK: - Private methods 213 | 214 | // MARK: Connection state changes 215 | 216 | /// The handler for managing changes to the `connection.state` via the `stateUpdateHandler` on a `NWConnection`. 217 | /// - Parameter state: The new `NWConnection.State` 218 | private func stateDidChange(to state: NWConnection.State) { 219 | switch state { 220 | case .ready: 221 | isMigratingConnection = false 222 | delegate?.webSocketDidConnect(connection: self) 223 | case .waiting(let error): 224 | isMigratingConnection = false 225 | reportErrorOrDisconnection(error) 226 | 227 | /// Workaround to prevent loop while reconnecting 228 | errorWhileWaitingCount += 1 229 | if errorWhileWaitingCount >= errorWhileWaitingLimit { 230 | tearDownConnection(error: error) 231 | errorWhileWaitingCount = 0 232 | } 233 | case .failed(let error): 234 | errorWhileWaitingCount = 0 235 | isMigratingConnection = false 236 | tearDownConnection(error: error) 237 | case .setup, .preparing: 238 | break 239 | case .cancelled: 240 | errorWhileWaitingCount = 0 241 | tearDownConnection(error: nil) 242 | @unknown default: 243 | fatalError() 244 | } 245 | } 246 | 247 | /// The handler for informing the `delegate` if there is a better network path available 248 | /// - Parameter isAvailable: `true` if a better network path is available. 249 | private func betterPath(isAvailable: Bool) { 250 | if isAvailable { 251 | migrateConnection { [weak self] result in 252 | guard let self = self else { 253 | return 254 | } 255 | 256 | self.delegate?.webSocketDidAttemptBetterPathMigration(result: result) 257 | } 258 | } 259 | } 260 | 261 | /// The handler for informing the `delegate` if the network connection viability has changed. 262 | /// - Parameter isViable: `true` if the network connection is viable. 263 | private func viabilityDidChange(isViable: Bool) { 264 | delegate?.webSocketViabilityDidChange(connection: self, isViable: isViable) 265 | } 266 | 267 | /// Attempts to migrate the active `connection` to a new one. 268 | /// 269 | /// Migrating can be useful if the active `connection` detects that a better network path has become available. 270 | /// - Parameter completionHandler: Returns a `Result`with the new connection if the migration was successful 271 | /// or a `NWError` if the migration failed for some reason. 272 | private func migrateConnection(completionHandler: @escaping (Result) -> Void) { 273 | guard !isMigratingConnection else { return } 274 | connection?.intentionalDisconnection = true 275 | connection?.cancel() 276 | isMigratingConnection = true 277 | connection = NWConnection(to: endpoint, using: parameters) 278 | connection?.stateUpdateHandler = { [weak self] state in 279 | self?.stateDidChange(to: state) 280 | } 281 | connection?.betterPathUpdateHandler = { [weak self] isAvailable in 282 | self?.betterPath(isAvailable: isAvailable) 283 | } 284 | connection?.viabilityUpdateHandler = { [weak self] isViable in 285 | self?.viabilityDidChange(isViable: isViable) 286 | } 287 | listen() 288 | connection?.start(queue: connectionQueue) 289 | } 290 | 291 | // MARK: Connection data transfer 292 | 293 | /// Receive a WebSocket message, and handle it according to it's metadata. 294 | /// - Parameters: 295 | /// - data: The `Data` that was received in the message. 296 | /// - context: `ContentContext` representing the received message, and its metadata. 297 | private func receiveMessage(data: Data, context: NWConnection.ContentContext) { 298 | guard let metadata = context.protocolMetadata.first as? NWProtocolWebSocket.Metadata else { 299 | return 300 | } 301 | 302 | switch metadata.opcode { 303 | case .binary: 304 | self.delegate?.webSocketDidReceiveMessage(connection: self, 305 | data: data) 306 | case .cont: 307 | // 308 | break 309 | case .text: 310 | guard let string = String(data: data, encoding: .utf8) else { 311 | return 312 | } 313 | self.delegate?.webSocketDidReceiveMessage(connection: self, 314 | string: string) 315 | case .close: 316 | scheduleDisconnectionReporting(closeCode: metadata.closeCode, 317 | reason: data) 318 | case .ping: 319 | // SEE `autoReplyPing = true` in `init()`. 320 | break 321 | case .pong: 322 | // SEE `ping()` FOR PONG RECEIVE LOGIC. 323 | break 324 | @unknown default: 325 | fatalError() 326 | } 327 | } 328 | 329 | /// Send some `Data` over the active `connection`. 330 | /// - Parameters: 331 | /// - data: Some `Data` to send (this should be formatted as binary or UTF-8 encoded text). 332 | /// - context: `ContentContext` representing the message to send, and its metadata. 333 | private func send(data: Data?, context: NWConnection.ContentContext) { 334 | connection?.send(content: data, 335 | contentContext: context, 336 | isComplete: true, 337 | completion: .contentProcessed({ [weak self] error in 338 | guard let self = self else { 339 | return 340 | } 341 | 342 | // If a connection closure was sent, inform delegate on completion 343 | if let socketMetadata = context.protocolMetadata.first as? NWProtocolWebSocket.Metadata, 344 | socketMetadata.opcode == .close { 345 | self.scheduleDisconnectionReporting(closeCode: socketMetadata.closeCode, 346 | reason: data) 347 | } 348 | 349 | if let error = error { 350 | self.reportErrorOrDisconnection(error) 351 | } 352 | })) 353 | } 354 | 355 | // MARK: Connection cleanup 356 | 357 | /// Schedules the reporting of a WebSocket disconnection. 358 | /// 359 | /// The disconnection will be actually reported once the underlying `NWConnection` has been fully torn down. 360 | /// - Parameters: 361 | /// - closeCode: A `NWProtocolWebSocket.CloseCode` describing how the connection closed. 362 | /// - reason: Optional extra information explaining the disconnection. (Formatted as UTF-8 encoded `Data`). 363 | private func scheduleDisconnectionReporting(closeCode: NWProtocolWebSocket.CloseCode, 364 | reason: Data?) { 365 | // Cancel any existing `disconnectionWorkItem` that was set first 366 | disconnectionWorkItem?.cancel() 367 | 368 | disconnectionWorkItem = DispatchWorkItem { [weak self] in 369 | guard let self = self else { return } 370 | self.delegate?.webSocketDidDisconnect(connection: self, 371 | closeCode: closeCode, 372 | reason: reason) 373 | } 374 | } 375 | 376 | /// Tear down the `connection`. 377 | /// 378 | /// This method should only be called in response to a `connection` which has entered either 379 | /// a `cancelled` or `failed` state within the `stateUpdateHandler` closure. 380 | /// - Parameter error: error description 381 | private func tearDownConnection(error: NWError?) { 382 | if let error = error, shouldReportNWError(error) { 383 | delegate?.webSocketDidReceiveError(connection: self, error: error) 384 | } 385 | pingTimer?.invalidate() 386 | connection?.cancel() 387 | connection = nil 388 | 389 | if let disconnectionWorkItem = disconnectionWorkItem { 390 | connectionQueue.async(execute: disconnectionWorkItem) 391 | } 392 | } 393 | 394 | /// Reports the `error` to the `delegate` (if appropriate) and if it represents an unexpected 395 | /// disconnection event, the disconnection will also be reported. 396 | /// - Parameter error: The `NWError` to inspect. 397 | private func reportErrorOrDisconnection(_ error: NWError) { 398 | if shouldReportNWError(error) { 399 | delegate?.webSocketDidReceiveError(connection: self, error: error) 400 | } 401 | 402 | if isDisconnectionNWError(error) { 403 | let reasonData = "The websocket disconnected unexpectedly".data(using: .utf8) 404 | scheduleDisconnectionReporting(closeCode: .protocolCode(.goingAway), 405 | reason: reasonData) 406 | } 407 | } 408 | 409 | /// Determine if a Network error should be reported. 410 | /// 411 | /// POSIX errors of either `ENOTCONN` ("Socket is not connected") or 412 | /// `ECANCELED` ("Operation canceled") should not be reported if the disconnection was intentional. 413 | /// All other errors should be reported. 414 | /// - Parameter error: The `NWError` to inspect. 415 | /// - Returns: `true` if the error should be reported. 416 | private func shouldReportNWError(_ error: NWError) -> Bool { 417 | if case let .posix(code) = error, 418 | code == .ENOTCONN || code == .ECANCELED, 419 | (connection?.intentionalDisconnection ?? false) { 420 | return false 421 | } else { 422 | return true 423 | } 424 | } 425 | 426 | /// Determine if a Network error represents an unexpected disconnection event. 427 | /// - Parameter error: The `NWError` to inspect. 428 | /// - Returns: `true` if the error represents an unexpected disconnection event. 429 | private func isDisconnectionNWError(_ error: NWError) -> Bool { 430 | if case let .posix(code) = error, 431 | code == .ETIMEDOUT 432 | || code == .ENOTCONN 433 | || code == .ECANCELED 434 | || code == .ENETDOWN 435 | || code == .ECONNABORTED { 436 | return true 437 | } else { 438 | return false 439 | } 440 | } 441 | } 442 | 443 | -------------------------------------------------------------------------------- /Sources/NWWebSocket/Protocol/WebSocketConnection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Network 3 | 4 | /// Defines a WebSocket connection. 5 | public protocol WebSocketConnection { 6 | /// Connect to the WebSocket. 7 | func connect() 8 | 9 | /// Send a UTF-8 formatted `String` over the WebSocket. 10 | /// - Parameter string: The `String` that will be sent. 11 | func send(string: String) 12 | 13 | /// Send some `Data` over the WebSocket. 14 | /// - Parameter data: The `Data` that will be sent. 15 | func send(data: Data) 16 | 17 | /// Start listening for messages over the WebSocket. 18 | func listen() 19 | 20 | /// Ping the WebSocket periodically. 21 | /// - Parameter interval: The `TimeInterval` (in seconds) with which to ping the server. 22 | func ping(interval: TimeInterval) 23 | 24 | /// Ping the WebSocket once. 25 | func ping() 26 | 27 | /// Disconnect from the WebSocket. 28 | /// - Parameter closeCode: The code to use when closing the WebSocket connection. 29 | func disconnect(closeCode: NWProtocolWebSocket.CloseCode) 30 | 31 | /// The WebSocket connection delegate. 32 | var delegate: WebSocketConnectionDelegate? { get set } 33 | } 34 | 35 | /// Defines a delegate for a WebSocket connection. 36 | public protocol WebSocketConnectionDelegate: AnyObject { 37 | /// Tells the delegate that the WebSocket did connect successfully. 38 | /// - Parameter connection: The active `WebSocketConnection`. 39 | func webSocketDidConnect(connection: WebSocketConnection) 40 | 41 | /// Tells the delegate that the WebSocket did disconnect. 42 | /// - Parameters: 43 | /// - connection: The `WebSocketConnection` that disconnected. 44 | /// - closeCode: A `NWProtocolWebSocket.CloseCode` describing how the connection closed. 45 | /// - reason: Optional extra information explaining the disconnection. (Formatted as UTF-8 encoded `Data`). 46 | func webSocketDidDisconnect(connection: WebSocketConnection, 47 | closeCode: NWProtocolWebSocket.CloseCode, 48 | reason: Data?) 49 | 50 | /// Tells the delegate that the WebSocket connection viability has changed. 51 | /// 52 | /// An example scenario of when this method would be called is a Wi-Fi connection being lost due to a device 53 | /// moving out of signal range, and then the method would be called again once the device moved back in range. 54 | /// - Parameters: 55 | /// - connection: The `WebSocketConnection` whose viability has changed. 56 | /// - isViable: A `Bool` indicating if the connection is viable or not. 57 | func webSocketViabilityDidChange(connection: WebSocketConnection, 58 | isViable: Bool) 59 | 60 | /// Tells the delegate that the WebSocket has attempted a migration based on a better network path becoming available. 61 | /// 62 | /// An example of when this method would be called is if a device is using a cellular connection, and a Wi-Fi connection 63 | /// becomes available. This method will also be called if a device loses a Wi-Fi connection, and a cellular connection is available. 64 | /// - Parameter result: A `Result` containing the `WebSocketConnection` if the migration was successful, or a 65 | /// `NWError` if the migration failed for some reason. 66 | func webSocketDidAttemptBetterPathMigration(result: Result) 67 | 68 | /// Tells the delegate that the WebSocket received an error. 69 | /// 70 | /// An error received by a WebSocket is not necessarily fatal. 71 | /// - Parameters: 72 | /// - connection: The `WebSocketConnection` that received an error. 73 | /// - error: The `NWError` that was received. 74 | func webSocketDidReceiveError(connection: WebSocketConnection, 75 | error: NWError) 76 | 77 | /// Tells the delegate that the WebSocket received a 'pong' from the server. 78 | /// - Parameter connection: The active `WebSocketConnection`. 79 | func webSocketDidReceivePong(connection: WebSocketConnection) 80 | 81 | /// Tells the delegate that the WebSocket received a `String` message. 82 | /// - Parameters: 83 | /// - connection: The active `WebSocketConnection`. 84 | /// - string: The UTF-8 formatted `String` that was received. 85 | func webSocketDidReceiveMessage(connection: WebSocketConnection, 86 | string: String) 87 | 88 | /// Tells the delegate that the WebSocket received a binary `Data` message. 89 | /// - Parameters: 90 | /// - connection: The active `WebSocketConnection`. 91 | /// - data: The `Data` that was received. 92 | func webSocketDidReceiveMessage(connection: WebSocketConnection, 93 | data: Data) 94 | } 95 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import NWWebSocketTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += NWWebSocketTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /Tests/NWWebSocketTests/NWWebSocketTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Network 3 | @testable import NWWebSocket 4 | 5 | class NWWebSocketTests: XCTestCase { 6 | static var socket: NWWebSocket! 7 | static var server: NWSwiftWebSocketServer! 8 | 9 | static var connectExpectation: XCTestExpectation? { 10 | didSet { 11 | Self.shouldDisconnectImmediately = false 12 | } 13 | } 14 | static var disconnectExpectation: XCTestExpectation! { 15 | didSet { 16 | Self.shouldDisconnectImmediately = true 17 | } 18 | } 19 | static var stringMessageExpectation: XCTestExpectation! { 20 | didSet { 21 | Self.shouldDisconnectImmediately = false 22 | } 23 | } 24 | static var dataMessageExpectation: XCTestExpectation! { 25 | didSet { 26 | Self.shouldDisconnectImmediately = false 27 | } 28 | } 29 | static var pongExpectation: XCTestExpectation? { 30 | didSet { 31 | Self.shouldDisconnectImmediately = false 32 | } 33 | } 34 | static var pingsWithIntervalExpectation: XCTestExpectation? { 35 | didSet { 36 | Self.shouldDisconnectImmediately = false 37 | } 38 | } 39 | static var errorExpectation: XCTestExpectation? { 40 | didSet { 41 | Self.shouldDisconnectImmediately = false 42 | } 43 | } 44 | 45 | static var shouldDisconnectImmediately: Bool! 46 | static var receivedPongTimestamps: [Date]! 47 | 48 | static let expectationTimeout = 5.0 49 | static let stringMessage = "This is a string message!" 50 | static let dataMessage = "This is a data message!".data(using: .utf8)! 51 | static let expectedReceivedPongsCount = 3 52 | static let repeatedPingInterval = 0.5 53 | static let validLocalhostServerPort: UInt16 = 3000 54 | static let invalidLocalhostServerPort: UInt16 = 2000 55 | 56 | override func setUp() { 57 | super.setUp() 58 | 59 | Self.server = NWSwiftWebSocketServer(port: Self.validLocalhostServerPort) 60 | try! Self.server.start() 61 | let serverURL = URL(string: "ws://localhost:\(Self.validLocalhostServerPort)")! 62 | Self.socket = NWWebSocket(url: serverURL) 63 | Self.socket.delegate = self 64 | Self.receivedPongTimestamps = [] 65 | } 66 | 67 | // MARK: - Test methods 68 | 69 | func testConnect() { 70 | Self.connectExpectation = XCTestExpectation(description: "connectExpectation") 71 | Self.socket.connect() 72 | wait(for: [Self.connectExpectation!], timeout: Self.expectationTimeout) 73 | } 74 | 75 | func testDisconnect() { 76 | Self.disconnectExpectation = XCTestExpectation(description: "disconnectExpectation") 77 | Self.socket.connect() 78 | wait(for: [Self.disconnectExpectation], timeout: Self.expectationTimeout) 79 | } 80 | 81 | func testReceiveStringMessage() { 82 | Self.stringMessageExpectation = XCTestExpectation(description: "stringMessageExpectation") 83 | Self.socket.connect() 84 | Self.socket.send(string: Self.stringMessage) 85 | wait(for: [Self.stringMessageExpectation], timeout: Self.expectationTimeout) 86 | } 87 | 88 | func testReceiveDataMessage() { 89 | Self.dataMessageExpectation = XCTestExpectation(description: "dataMessageExpectation") 90 | Self.socket.connect() 91 | Self.socket.send(data: Self.dataMessage) 92 | wait(for: [Self.dataMessageExpectation], timeout: Self.expectationTimeout) 93 | } 94 | 95 | func testReceivePong() { 96 | Self.pongExpectation = XCTestExpectation(description: "pongExpectation") 97 | Self.socket.connect() 98 | Self.socket.ping() 99 | wait(for: [Self.pongExpectation!], timeout: Self.expectationTimeout) 100 | } 101 | 102 | func testPingsWithInterval() { 103 | Self.pingsWithIntervalExpectation = XCTestExpectation(description: "pingsWithIntervalExpectation") 104 | Self.socket.connect() 105 | Self.socket.ping(interval: Self.repeatedPingInterval) 106 | wait(for: [Self.pingsWithIntervalExpectation!], timeout: Self.expectationTimeout) 107 | } 108 | 109 | func testReceiveError() { 110 | // Redefine socket with invalid path 111 | Self.socket = NWWebSocket(request: URLRequest(url: URL(string: "ws://localhost:\(Self.invalidLocalhostServerPort)")!)) 112 | Self.socket.delegate = self 113 | 114 | Self.errorExpectation = XCTestExpectation(description: "errorExpectation") 115 | Self.socket.connect() 116 | wait(for: [Self.errorExpectation!], timeout: Self.expectationTimeout) 117 | } 118 | 119 | } 120 | 121 | // MARK: - WebSocketConnectionDelegate conformance 122 | 123 | extension NWWebSocketTests: WebSocketConnectionDelegate { 124 | 125 | func webSocketDidConnect(connection: WebSocketConnection) { 126 | Self.connectExpectation?.fulfill() 127 | 128 | if Self.shouldDisconnectImmediately { 129 | Self.socket.disconnect() 130 | } 131 | } 132 | 133 | func webSocketDidDisconnect(connection: WebSocketConnection, 134 | closeCode: NWProtocolWebSocket.CloseCode, reason: Data?) { 135 | Self.disconnectExpectation.fulfill() 136 | } 137 | 138 | func webSocketViabilityDidChange(connection: WebSocketConnection, isViable: Bool) { 139 | if isViable == false { 140 | XCTFail("WebSocket should not become unviable during testing.") 141 | } 142 | } 143 | 144 | func webSocketDidAttemptBetterPathMigration(result: Result) { 145 | XCTFail("WebSocket should not attempt to migrate to a better path during testing.") 146 | } 147 | 148 | func webSocketDidReceiveError(connection: WebSocketConnection, error: NWError) { 149 | Self.errorExpectation?.fulfill() 150 | } 151 | 152 | func webSocketDidReceivePong(connection: WebSocketConnection) { 153 | Self.pongExpectation?.fulfill() 154 | 155 | guard Self.pingsWithIntervalExpectation != nil else { 156 | return 157 | } 158 | 159 | if Self.receivedPongTimestamps.count == Self.expectedReceivedPongsCount { 160 | Self.pingsWithIntervalExpectation?.fulfill() 161 | } 162 | Self.receivedPongTimestamps.append(Date()) 163 | } 164 | 165 | func webSocketDidReceiveMessage(connection: WebSocketConnection, string: String) { 166 | XCTAssertEqual(string, Self.stringMessage) 167 | Self.stringMessageExpectation.fulfill() 168 | } 169 | 170 | func webSocketDidReceiveMessage(connection: WebSocketConnection, data: Data) { 171 | XCTAssertEqual(data, Self.dataMessage) 172 | Self.dataMessageExpectation.fulfill() 173 | } 174 | } 175 | 176 | -------------------------------------------------------------------------------- /Tests/NWWebSocketTests/Server/NWServerConnection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Network 3 | 4 | internal class NWServerConnection { 5 | 6 | // MARK: - Public properties 7 | 8 | let id: Int 9 | 10 | var didStopHandler: ((Error?) -> Void)? = nil 11 | var didReceiveStringHandler: ((String) -> ())? = nil 12 | var didReceiveDataHandler: ((Data) -> ())? = nil 13 | 14 | // MARK: - Private properties 15 | 16 | private static var nextID: Int = 0 17 | private let connection: NWConnection 18 | 19 | // MARK: - Lifecycle 20 | 21 | init(nwConnection: NWConnection) { 22 | connection = nwConnection 23 | id = Self.nextID 24 | Self.nextID += 1 25 | } 26 | 27 | deinit { 28 | print("deinit") 29 | } 30 | 31 | // MARK: - Public methods 32 | 33 | func start() { 34 | print("connection \(id) will start") 35 | connection.stateUpdateHandler = self.stateDidChange(to:) 36 | listen() 37 | connection.start(queue: .main) 38 | } 39 | 40 | func receiveMessage(data: Data, context: NWConnection.ContentContext) { 41 | guard let metadata = context.protocolMetadata.first as? NWProtocolWebSocket.Metadata else { 42 | return 43 | } 44 | 45 | switch metadata.opcode { 46 | case .binary: 47 | didReceiveDataHandler?(data) 48 | case .cont: 49 | // 50 | break 51 | case .text: 52 | guard let string = String(data: data, encoding: .utf8) else { 53 | return 54 | } 55 | didReceiveStringHandler?(string) 56 | case .close: 57 | // 58 | break 59 | case .ping: 60 | pong() 61 | break 62 | case .pong: 63 | // 64 | break 65 | @unknown default: 66 | fatalError() 67 | } 68 | } 69 | 70 | func send(string: String) { 71 | let metaData = NWProtocolWebSocket.Metadata(opcode: .text) 72 | let context = NWConnection.ContentContext(identifier: "textContext", 73 | metadata: [metaData]) 74 | self.send(data: string.data(using: .utf8), context: context) 75 | } 76 | 77 | func send(data: Data) { 78 | let metaData = NWProtocolWebSocket.Metadata(opcode: .binary) 79 | let context = NWConnection.ContentContext(identifier: "binaryContext", 80 | metadata: [metaData]) 81 | self.send(data: data, context: context) 82 | } 83 | 84 | func stop() { 85 | print("connection \(id) will stop") 86 | } 87 | 88 | // MARK: - Private methods 89 | 90 | private func stateDidChange(to state: NWConnection.State) { 91 | switch state { 92 | case .setup: 93 | print("connection is setup.") 94 | case .waiting(let error): 95 | connectionDidReceiveError(error) 96 | case .preparing: 97 | print("connection is being establised.") 98 | case .ready: 99 | print("connection \(id) ready") 100 | case .cancelled: 101 | stopConnection(error: nil) 102 | case .failed(let error): 103 | stopConnection(error: error) 104 | @unknown default: 105 | fatalError() 106 | } 107 | } 108 | 109 | private func listen() { 110 | connection.receiveMessage() { (data, context, isComplete, error) in 111 | if let data = data, let context = context, !data.isEmpty { 112 | self.receiveMessage(data: data, context: context) 113 | } 114 | if let error = error { 115 | self.connectionDidReceiveError(error) 116 | } else { 117 | self.listen() 118 | } 119 | } 120 | } 121 | 122 | private func pong() { 123 | let metaData = NWProtocolWebSocket.Metadata(opcode: .pong) 124 | let context = NWConnection.ContentContext(identifier: "pongContext", 125 | metadata: [metaData]) 126 | self.send(data: Data(), context: context) 127 | } 128 | 129 | private func send(data: Data?, context: NWConnection.ContentContext) { 130 | self.connection.send(content: data, 131 | contentContext: context, 132 | isComplete: true, 133 | completion: .contentProcessed( { error in 134 | if let error = error { 135 | self.connectionDidReceiveError(error) 136 | return 137 | } 138 | print("connection \(self.id) did send, data: \(String(describing: data))") 139 | })) 140 | } 141 | 142 | private func connectionDidReceiveError(_ error: NWError) { 143 | print("connection did receive error: \(error.debugDescription)") 144 | } 145 | 146 | private func stopConnection(error: Error?) { 147 | connection.stateUpdateHandler = nil 148 | if let didStopHandler = didStopHandler { 149 | self.didStopHandler = nil 150 | didStopHandler(error) 151 | } 152 | if let error = error { 153 | print("connection \(id) did fail, error: \(error)") 154 | } else { 155 | print("connection \(id) did end") 156 | } 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /Tests/NWWebSocketTests/Server/NWWebSocketServer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Network 3 | 4 | internal class NWSwiftWebSocketServer { 5 | 6 | // MARK: - Private properties 7 | 8 | private let port: NWEndpoint.Port 9 | private var listener: NWListener? 10 | private let parameters: NWParameters 11 | private var connectionsByID: [Int: NWServerConnection] = [:] 12 | 13 | // MARK: - Lifecycle 14 | 15 | init(port: UInt16) { 16 | self.port = NWEndpoint.Port(rawValue: port)! 17 | parameters = NWParameters(tls: nil) 18 | parameters.allowLocalEndpointReuse = true 19 | parameters.includePeerToPeer = true 20 | let wsOptions = NWProtocolWebSocket.Options() 21 | wsOptions.autoReplyPing = true 22 | parameters.defaultProtocolStack.applicationProtocols.insert(wsOptions, at: 0) 23 | } 24 | 25 | // MARK: - Public methods 26 | 27 | func start() throws { 28 | print("Server starting...") 29 | if listener == nil { 30 | listener = try! NWListener(using: parameters, on: self.port) 31 | } 32 | listener?.stateUpdateHandler = self.stateDidChange(to:) 33 | listener?.newConnectionHandler = self.didAccept(nwConnection:) 34 | listener?.start(queue: .main) 35 | } 36 | 37 | func stop() { 38 | listener?.cancel() 39 | } 40 | 41 | // MARK: - Private methods 42 | 43 | private func didAccept(nwConnection: NWConnection) { 44 | let connection = NWServerConnection(nwConnection: nwConnection) 45 | connectionsByID[connection.id] = connection 46 | 47 | connection.start() 48 | 49 | connection.didStopHandler = { err in 50 | if let err = err { 51 | print(err) 52 | } 53 | self.connectionDidStop(connection) 54 | } 55 | connection.didReceiveStringHandler = { string in 56 | self.connectionsByID.values.forEach { connection in 57 | print("sent \(string) to open connection \(connection.id)") 58 | connection.send(string: string) 59 | } 60 | } 61 | connection.didReceiveDataHandler = { data in 62 | self.connectionsByID.values.forEach { connection in 63 | print("sent \(String(data: data, encoding: .utf8) ?? "NOTHING") to open connection \(connection.id)") 64 | connection.send(data: data) 65 | } 66 | } 67 | 68 | print("server did open connection \(connection.id)") 69 | } 70 | 71 | private func stateDidChange(to newState: NWListener.State) { 72 | switch newState { 73 | case .setup: 74 | print("Server is setup.") 75 | case .waiting(let error): 76 | print("Server is waiting to start, non-fatal error: \(error.debugDescription)") 77 | case .ready: 78 | print("Server ready.") 79 | case .cancelled: 80 | self.stopSever(error: nil) 81 | case .failed(let error): 82 | self.stopSever(error: error) 83 | @unknown default: 84 | fatalError() 85 | } 86 | } 87 | 88 | private func connectionDidStop(_ connection: NWServerConnection) { 89 | self.connectionsByID.removeValue(forKey: connection.id) 90 | print("server did close connection \(connection.id)") 91 | } 92 | 93 | private func stopSever(error: NWError?) { 94 | self.listener = nil 95 | for connection in self.connectionsByID.values { 96 | connection.didStopHandler = nil 97 | connection.stop() 98 | } 99 | self.connectionsByID.removeAll() 100 | if let error = error { 101 | print("Server failure, error: \(error.debugDescription)") 102 | } else { 103 | print("Server stopped normally.") 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/NWWebSocketTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension NWWebSocketTests { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__NWWebSocketTests = [ 9 | ("testConnect", testConnect), 10 | ("testDisconnect", testDisconnect), 11 | ("testPingsWithInterval", testPingsWithInterval), 12 | ("testReceiveDataMessage", testReceiveDataMessage), 13 | ("testReceiveError", testReceiveError), 14 | ("testReceivePong", testReceivePong), 15 | ("testReceiveStringMessage", testReceiveStringMessage), 16 | ] 17 | } 18 | 19 | public func __allTests() -> [XCTestCaseEntry] { 20 | return [ 21 | testCase(NWWebSocketTests.__allTests__NWWebSocketTests), 22 | ] 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /docs/Classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Classes Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

NWWebSocket 0.5.2 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 60 |
61 |
62 |
63 |

Classes

64 |

The following classes are available globally.

65 | 66 |
67 |
68 |
69 |
    70 |
  • 71 |
    72 | 73 | 74 | 75 | NWWebSocket 76 | 77 |
    78 |
    79 |
    80 |
    81 |
    82 |
    83 |

    A WebSocket client that manages a socket connection.

    84 | 85 | See more 86 |
    87 |
    88 |

    Declaration

    89 |
    90 |

    Swift

    91 |
    open class NWWebSocket : WebSocketConnection
    92 | 93 |
    94 |
    95 |
    96 | Show on GitHub 97 |
    98 |
    99 |
    100 |
  • 101 |
102 |
103 |
104 |
105 | 109 |
110 |
111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /docs/Protocols.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Protocols Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

NWWebSocket 0.5.2 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 60 |
61 |
62 |
63 |

Protocols

64 |

The following protocols are available globally.

65 | 66 |
67 |
68 |
69 |
    70 |
  • 71 |
    72 | 73 | 74 | 75 | WebSocketConnection 76 | 77 |
    78 |
    79 |
    80 |
    81 |
    82 |
    83 |

    Defines a WebSocket connection.

    84 | 85 | See more 86 |
    87 |
    88 |

    Declaration

    89 |
    90 |

    Swift

    91 |
    public protocol WebSocketConnection
    92 | 93 |
    94 |
    95 |
    96 | Show on GitHub 97 |
    98 |
    99 |
    100 |
  • 101 |
  • 102 |
    103 | 104 | 105 | 106 | WebSocketConnectionDelegate 107 | 108 |
    109 |
    110 |
    111 |
    112 |
    113 |
    114 |

    Defines a delegate for a WebSocket connection.

    115 | 116 | See more 117 |
    118 |
    119 |

    Declaration

    120 |
    121 |

    Swift

    122 |
    public protocol WebSocketConnectionDelegate : AnyObject
    123 | 124 |
    125 |
    126 |
    127 | Show on GitHub 128 |
    129 |
    130 |
    131 |
  • 132 |
133 |
134 |
135 |
136 | 140 |
141 |
142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/Protocols/WebSocketConnection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocketConnection Protocol Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

NWWebSocket 0.5.2 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 60 |
61 |
62 |
63 |

WebSocketConnection

64 |
65 |
66 | 67 |
public protocol WebSocketConnection
68 | 69 |
70 |
71 |

Defines a WebSocket connection.

72 | 73 |
74 | Show on GitHub 75 |
76 |
77 |
78 |
79 |
    80 |
  • 81 |
    82 | 83 | 84 | 85 | connect() 86 | 87 |
    88 |
    89 |
    90 |
    91 |
    92 |
    93 |

    Connect to the WebSocket.

    94 | 95 |
    96 |
    97 |

    Declaration

    98 |
    99 |

    Swift

    100 |
    func connect()
    101 | 102 |
    103 |
    104 |
    105 | Show on GitHub 106 |
    107 |
    108 |
    109 |
  • 110 |
  • 111 |
    112 | 113 | 114 | 115 | send(string:) 116 | 117 |
    118 |
    119 |
    120 |
    121 |
    122 |
    123 |

    Send a UTF-8 formatted String over the WebSocket.

    124 | 125 |
    126 |
    127 |

    Declaration

    128 |
    129 |

    Swift

    130 |
    func send(string: String)
    131 | 132 |
    133 |
    134 |
    135 |

    Parameters

    136 | 137 | 138 | 139 | 144 | 149 | 150 | 151 |
    140 | 141 | string 142 | 143 | 145 |
    146 |

    The String that will be sent.

    147 |
    148 |
    152 |
    153 |
    154 | Show on GitHub 155 |
    156 |
    157 |
    158 |
  • 159 |
  • 160 |
    161 | 162 | 163 | 164 | send(data:) 165 | 166 |
    167 |
    168 |
    169 |
    170 |
    171 |
    172 |

    Send some Data over the WebSocket.

    173 | 174 |
    175 |
    176 |

    Declaration

    177 |
    178 |

    Swift

    179 |
    func send(data: Data)
    180 | 181 |
    182 |
    183 |
    184 |

    Parameters

    185 | 186 | 187 | 188 | 193 | 198 | 199 | 200 |
    189 | 190 | data 191 | 192 | 194 |
    195 |

    The Data that will be sent.

    196 |
    197 |
    201 |
    202 |
    203 | Show on GitHub 204 |
    205 |
    206 |
    207 |
  • 208 |
  • 209 |
    210 | 211 | 212 | 213 | listen() 214 | 215 |
    216 |
    217 |
    218 |
    219 |
    220 |
    221 |

    Start listening for messages over the WebSocket.

    222 | 223 |
    224 |
    225 |

    Declaration

    226 |
    227 |

    Swift

    228 |
    func listen()
    229 | 230 |
    231 |
    232 |
    233 | Show on GitHub 234 |
    235 |
    236 |
    237 |
  • 238 |
  • 239 |
    240 | 241 | 242 | 243 | ping(interval:) 244 | 245 |
    246 |
    247 |
    248 |
    249 |
    250 |
    251 |

    Ping the WebSocket periodically.

    252 | 253 |
    254 |
    255 |

    Declaration

    256 |
    257 |

    Swift

    258 |
    func ping(interval: TimeInterval)
    259 | 260 |
    261 |
    262 |
    263 |

    Parameters

    264 | 265 | 266 | 267 | 272 | 277 | 278 | 279 |
    268 | 269 | interval 270 | 271 | 273 |
    274 |

    The TimeInterval (in seconds) with which to ping the server.

    275 |
    276 |
    280 |
    281 |
    282 | Show on GitHub 283 |
    284 |
    285 |
    286 |
  • 287 |
  • 288 |
    289 | 290 | 291 | 292 | ping() 293 | 294 |
    295 |
    296 |
    297 |
    298 |
    299 |
    300 |

    Ping the WebSocket once.

    301 | 302 |
    303 |
    304 |

    Declaration

    305 |
    306 |

    Swift

    307 |
    func ping()
    308 | 309 |
    310 |
    311 |
    312 | Show on GitHub 313 |
    314 |
    315 |
    316 |
  • 317 |
  • 318 |
    319 | 320 | 321 | 322 | disconnect(closeCode:) 323 | 324 |
    325 |
    326 |
    327 |
    328 |
    329 |
    330 |

    Disconnect from the WebSocket.

    331 | 332 |
    333 |
    334 |

    Declaration

    335 |
    336 |

    Swift

    337 |
    func disconnect(closeCode: NWProtocolWebSocket.CloseCode)
    338 | 339 |
    340 |
    341 |
    342 |

    Parameters

    343 | 344 | 345 | 346 | 351 | 356 | 357 | 358 |
    347 | 348 | closeCode 349 | 350 | 352 |
    353 |

    The code to use when closing the WebSocket connection.

    354 |
    355 |
    359 |
    360 |
    361 | Show on GitHub 362 |
    363 |
    364 |
    365 |
  • 366 |
  • 367 |
    368 | 369 | 370 | 371 | delegate 372 | 373 |
    374 |
    375 |
    376 |
    377 |
    378 |
    379 |

    The WebSocket connection delegate.

    380 | 381 |
    382 |
    383 |

    Declaration

    384 |
    385 |

    Swift

    386 |
    var delegate: WebSocketConnectionDelegate? { get set }
    387 | 388 |
    389 |
    390 |
    391 | Show on GitHub 392 |
    393 |
    394 |
    395 |
  • 396 |
397 |
398 |
399 |
400 | 404 |
405 |
406 | 407 | 408 | 409 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 100% 23 | 24 | 25 | 100% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { 2 | background: transparent; 3 | border: 0; 4 | margin: 0; 5 | outline: 0; 6 | padding: 0; 7 | vertical-align: baseline; } 8 | 9 | body { 10 | background-color: #f2f2f2; 11 | font-family: Helvetica, freesans, Arial, sans-serif; 12 | font-size: 14px; 13 | -webkit-font-smoothing: subpixel-antialiased; 14 | word-wrap: break-word; } 15 | 16 | h1, h2, h3 { 17 | margin-top: 0.8em; 18 | margin-bottom: 0.3em; 19 | font-weight: 100; 20 | color: black; } 21 | 22 | h1 { 23 | font-size: 2.5em; } 24 | 25 | h2 { 26 | font-size: 2em; 27 | border-bottom: 1px solid #e2e2e2; } 28 | 29 | h4 { 30 | font-size: 13px; 31 | line-height: 1.5; 32 | margin-top: 21px; } 33 | 34 | h5 { 35 | font-size: 1.1em; } 36 | 37 | h6 { 38 | font-size: 1.1em; 39 | color: #777; } 40 | 41 | .section-name { 42 | color: gray; 43 | display: block; 44 | font-family: Helvetica; 45 | font-size: 22px; 46 | font-weight: 100; 47 | margin-bottom: 15px; } 48 | 49 | pre, code { 50 | font: 0.95em Menlo, monospace; 51 | color: #777; 52 | word-wrap: normal; } 53 | 54 | p code, li code { 55 | background-color: #eee; 56 | padding: 2px 4px; 57 | border-radius: 4px; } 58 | 59 | pre > code { 60 | padding: 0; } 61 | 62 | a { 63 | color: #0088cc; 64 | text-decoration: none; } 65 | a code { 66 | color: inherit; } 67 | 68 | ul { 69 | padding-left: 15px; } 70 | 71 | li { 72 | line-height: 1.8em; } 73 | 74 | img { 75 | max-width: 100%; } 76 | 77 | blockquote { 78 | margin-left: 0; 79 | padding: 0 10px; 80 | border-left: 4px solid #ccc; } 81 | 82 | .content-wrapper { 83 | margin: 0 auto; 84 | width: 980px; } 85 | 86 | header { 87 | font-size: 0.85em; 88 | line-height: 32px; 89 | background-color: #414141; 90 | position: fixed; 91 | width: 100%; 92 | z-index: 3; } 93 | header img { 94 | padding-right: 6px; 95 | vertical-align: -4px; 96 | height: 16px; } 97 | header a { 98 | color: #fff; } 99 | header p { 100 | float: left; 101 | color: #999; } 102 | header .header-right { 103 | float: right; 104 | margin-left: 16px; } 105 | 106 | #breadcrumbs { 107 | background-color: #f2f2f2; 108 | height: 21px; 109 | padding-top: 17px; 110 | position: fixed; 111 | width: 100%; 112 | z-index: 2; 113 | margin-top: 32px; } 114 | #breadcrumbs #carat { 115 | height: 10px; 116 | margin: 0 5px; } 117 | 118 | .sidebar { 119 | background-color: #f9f9f9; 120 | border: 1px solid #e2e2e2; 121 | overflow-y: auto; 122 | overflow-x: hidden; 123 | position: fixed; 124 | top: 70px; 125 | bottom: 0; 126 | width: 230px; 127 | word-wrap: normal; } 128 | 129 | .nav-groups { 130 | list-style-type: none; 131 | background: #fff; 132 | padding-left: 0; } 133 | 134 | .nav-group-name { 135 | border-bottom: 1px solid #e2e2e2; 136 | font-size: 1.1em; 137 | font-weight: 100; 138 | padding: 15px 0 15px 20px; } 139 | .nav-group-name > a { 140 | color: #333; } 141 | 142 | .nav-group-tasks { 143 | margin-top: 5px; } 144 | 145 | .nav-group-task { 146 | font-size: 0.9em; 147 | list-style-type: none; 148 | white-space: nowrap; } 149 | .nav-group-task a { 150 | color: #888; } 151 | 152 | .main-content { 153 | background-color: #fff; 154 | border: 1px solid #e2e2e2; 155 | margin-left: 246px; 156 | position: absolute; 157 | overflow: hidden; 158 | padding-bottom: 20px; 159 | top: 70px; 160 | width: 734px; } 161 | .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { 162 | margin-bottom: 1em; } 163 | .main-content p { 164 | line-height: 1.8em; } 165 | .main-content section .section:first-child { 166 | margin-top: 0; 167 | padding-top: 0; } 168 | .main-content section .task-group-section .task-group:first-of-type { 169 | padding-top: 10px; } 170 | .main-content section .task-group-section .task-group:first-of-type .section-name { 171 | padding-top: 15px; } 172 | .main-content section .heading:before { 173 | content: ""; 174 | display: block; 175 | padding-top: 70px; 176 | margin: -70px 0 0; } 177 | .main-content .section-name p { 178 | margin-bottom: inherit; 179 | line-height: inherit; } 180 | .main-content .section-name code { 181 | background-color: inherit; 182 | padding: inherit; 183 | color: inherit; } 184 | 185 | .section { 186 | padding: 0 25px; } 187 | 188 | .highlight { 189 | background-color: #eee; 190 | padding: 10px 12px; 191 | border: 1px solid #e2e2e2; 192 | border-radius: 4px; 193 | overflow-x: auto; } 194 | 195 | .declaration .highlight { 196 | overflow-x: initial; 197 | padding: 0 40px 40px 0; 198 | margin-bottom: -25px; 199 | background-color: transparent; 200 | border: none; } 201 | 202 | .section-name { 203 | margin: 0; 204 | margin-left: 18px; } 205 | 206 | .task-group-section { 207 | margin-top: 10px; 208 | padding-left: 6px; 209 | border-top: 1px solid #e2e2e2; } 210 | 211 | .task-group { 212 | padding-top: 0px; } 213 | 214 | .task-name-container a[name]:before { 215 | content: ""; 216 | display: block; 217 | padding-top: 70px; 218 | margin: -70px 0 0; } 219 | 220 | .section-name-container { 221 | position: relative; 222 | display: inline-block; } 223 | .section-name-container .section-name-link { 224 | position: absolute; 225 | top: 0; 226 | left: 0; 227 | bottom: 0; 228 | right: 0; 229 | margin-bottom: 0; } 230 | .section-name-container .section-name { 231 | position: relative; 232 | pointer-events: none; 233 | z-index: 1; } 234 | .section-name-container .section-name a { 235 | pointer-events: auto; } 236 | 237 | .item { 238 | padding-top: 8px; 239 | width: 100%; 240 | list-style-type: none; } 241 | .item a[name]:before { 242 | content: ""; 243 | display: block; 244 | padding-top: 70px; 245 | margin: -70px 0 0; } 246 | .item code { 247 | background-color: transparent; 248 | padding: 0; } 249 | .item .token, .item .direct-link { 250 | display: inline-block; 251 | text-indent: -20px; 252 | padding-left: 3px; 253 | margin-left: 35px; 254 | font-size: 11.9px; 255 | transition: all 300ms; } 256 | .item .token-open { 257 | margin-left: 20px; } 258 | .item .discouraged { 259 | text-decoration: line-through; } 260 | .item .declaration-note { 261 | font-size: .85em; 262 | color: gray; 263 | font-style: italic; } 264 | 265 | .pointer-container { 266 | border-bottom: 1px solid #e2e2e2; 267 | left: -23px; 268 | padding-bottom: 13px; 269 | position: relative; 270 | width: 110%; } 271 | 272 | .pointer { 273 | background: #f9f9f9; 274 | border-left: 1px solid #e2e2e2; 275 | border-top: 1px solid #e2e2e2; 276 | height: 12px; 277 | left: 21px; 278 | top: -7px; 279 | -webkit-transform: rotate(45deg); 280 | -moz-transform: rotate(45deg); 281 | -o-transform: rotate(45deg); 282 | transform: rotate(45deg); 283 | position: absolute; 284 | width: 12px; } 285 | 286 | .height-container { 287 | display: none; 288 | left: -25px; 289 | padding: 0 25px; 290 | position: relative; 291 | width: 100%; 292 | overflow: hidden; } 293 | .height-container .section { 294 | background: #f9f9f9; 295 | border-bottom: 1px solid #e2e2e2; 296 | left: -25px; 297 | position: relative; 298 | width: 100%; 299 | padding-top: 10px; 300 | padding-bottom: 5px; } 301 | 302 | .aside, .language { 303 | padding: 6px 12px; 304 | margin: 12px 0; 305 | border-left: 5px solid #dddddd; 306 | overflow-y: hidden; } 307 | .aside .aside-title, .language .aside-title { 308 | font-size: 9px; 309 | letter-spacing: 2px; 310 | text-transform: uppercase; 311 | padding-bottom: 0; 312 | margin: 0; 313 | color: #aaa; 314 | -webkit-user-select: none; } 315 | .aside p:last-child, .language p:last-child { 316 | margin-bottom: 0; } 317 | 318 | .language { 319 | border-left: 5px solid #cde9f4; } 320 | .language .aside-title { 321 | color: #4b8afb; } 322 | 323 | .aside-warning, .aside-deprecated, .aside-unavailable { 324 | border-left: 5px solid #ff6666; } 325 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 326 | color: #ff0000; } 327 | 328 | .graybox { 329 | border-collapse: collapse; 330 | width: 100%; } 331 | .graybox p { 332 | margin: 0; 333 | word-break: break-word; 334 | min-width: 50px; } 335 | .graybox td { 336 | border: 1px solid #e2e2e2; 337 | padding: 5px 25px 5px 10px; 338 | vertical-align: middle; } 339 | .graybox tr td:first-of-type { 340 | text-align: right; 341 | padding: 7px; 342 | vertical-align: top; 343 | word-break: normal; 344 | width: 40px; } 345 | 346 | .slightly-smaller { 347 | font-size: 0.9em; } 348 | 349 | #footer { 350 | position: relative; 351 | top: 10px; 352 | bottom: 0px; 353 | margin-left: 25px; } 354 | #footer p { 355 | margin: 0; 356 | color: #aaa; 357 | font-size: 0.8em; } 358 | 359 | html.dash header, html.dash #breadcrumbs, html.dash .sidebar { 360 | display: none; } 361 | 362 | html.dash .main-content { 363 | width: 980px; 364 | margin-left: 0; 365 | border: none; 366 | width: 100%; 367 | top: 0; 368 | padding-bottom: 0; } 369 | 370 | html.dash .height-container { 371 | display: block; } 372 | 373 | html.dash .item .token { 374 | margin-left: 0; } 375 | 376 | html.dash .content-wrapper { 377 | width: auto; } 378 | 379 | html.dash #footer { 380 | position: static; } 381 | 382 | form[role=search] { 383 | float: right; } 384 | form[role=search] input { 385 | font: Helvetica, freesans, Arial, sans-serif; 386 | margin-top: 6px; 387 | font-size: 13px; 388 | line-height: 20px; 389 | padding: 0px 10px; 390 | border: none; 391 | border-radius: 1em; } 392 | .loading form[role=search] input { 393 | background: white url(../img/spinner.gif) center right 4px no-repeat; } 394 | form[role=search] .tt-menu { 395 | margin: 0; 396 | min-width: 300px; 397 | background: #fff; 398 | color: #333; 399 | border: 1px solid #e2e2e2; 400 | z-index: 4; } 401 | form[role=search] .tt-highlight { 402 | font-weight: bold; } 403 | form[role=search] .tt-suggestion { 404 | font: Helvetica, freesans, Arial, sans-serif; 405 | font-size: 14px; 406 | padding: 0 8px; } 407 | form[role=search] .tt-suggestion span { 408 | display: table-cell; 409 | white-space: nowrap; } 410 | form[role=search] .tt-suggestion .doc-parent-name { 411 | width: 100%; 412 | text-align: right; 413 | font-weight: normal; 414 | font-size: 0.9em; 415 | padding-left: 16px; } 416 | form[role=search] .tt-suggestion:hover, 417 | form[role=search] .tt-suggestion.tt-cursor { 418 | cursor: pointer; 419 | background-color: #4183c4; 420 | color: #fff; } 421 | form[role=search] .tt-suggestion:hover .doc-parent-name, 422 | form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { 423 | color: #fff; } 424 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.nwwebsocket 7 | CFBundleName 8 | NWWebSocket 9 | DocSetPlatformFamily 10 | nwwebsocket 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/Classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Classes Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

NWWebSocket 0.5.2 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 60 |
61 |
62 |
63 |

Classes

64 |

The following classes are available globally.

65 | 66 |
67 |
68 |
69 |
    70 |
  • 71 |
    72 | 73 | 74 | 75 | NWWebSocket 76 | 77 |
    78 |
    79 |
    80 |
    81 |
    82 |
    83 |

    A WebSocket client that manages a socket connection.

    84 | 85 | See more 86 |
    87 |
    88 |

    Declaration

    89 |
    90 |

    Swift

    91 |
    open class NWWebSocket : WebSocketConnection
    92 | 93 |
    94 |
    95 |
    96 | Show on GitHub 97 |
    98 |
    99 |
    100 |
  • 101 |
102 |
103 |
104 |
105 | 109 |
110 |
111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/Protocols.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Protocols Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

NWWebSocket 0.5.2 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 60 |
61 |
62 |
63 |

Protocols

64 |

The following protocols are available globally.

65 | 66 |
67 |
68 |
69 |
    70 |
  • 71 |
    72 | 73 | 74 | 75 | WebSocketConnection 76 | 77 |
    78 |
    79 |
    80 |
    81 |
    82 |
    83 |

    Defines a WebSocket connection.

    84 | 85 | See more 86 |
    87 |
    88 |

    Declaration

    89 |
    90 |

    Swift

    91 |
    public protocol WebSocketConnection
    92 | 93 |
    94 |
    95 |
    96 | Show on GitHub 97 |
    98 |
    99 |
    100 |
  • 101 |
  • 102 |
    103 | 104 | 105 | 106 | WebSocketConnectionDelegate 107 | 108 |
    109 |
    110 |
    111 |
    112 |
    113 |
    114 |

    Defines a delegate for a WebSocket connection.

    115 | 116 | See more 117 |
    118 |
    119 |

    Declaration

    120 |
    121 |

    Swift

    122 |
    public protocol WebSocketConnectionDelegate : AnyObject
    123 | 124 |
    125 |
    126 |
    127 | Show on GitHub 128 |
    129 |
    130 |
    131 |
  • 132 |
133 |
134 |
135 |
136 | 140 |
141 |
142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 100% 23 | 24 | 25 | 100% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/css/jazzy.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { 2 | background: transparent; 3 | border: 0; 4 | margin: 0; 5 | outline: 0; 6 | padding: 0; 7 | vertical-align: baseline; } 8 | 9 | body { 10 | background-color: #f2f2f2; 11 | font-family: Helvetica, freesans, Arial, sans-serif; 12 | font-size: 14px; 13 | -webkit-font-smoothing: subpixel-antialiased; 14 | word-wrap: break-word; } 15 | 16 | h1, h2, h3 { 17 | margin-top: 0.8em; 18 | margin-bottom: 0.3em; 19 | font-weight: 100; 20 | color: black; } 21 | 22 | h1 { 23 | font-size: 2.5em; } 24 | 25 | h2 { 26 | font-size: 2em; 27 | border-bottom: 1px solid #e2e2e2; } 28 | 29 | h4 { 30 | font-size: 13px; 31 | line-height: 1.5; 32 | margin-top: 21px; } 33 | 34 | h5 { 35 | font-size: 1.1em; } 36 | 37 | h6 { 38 | font-size: 1.1em; 39 | color: #777; } 40 | 41 | .section-name { 42 | color: gray; 43 | display: block; 44 | font-family: Helvetica; 45 | font-size: 22px; 46 | font-weight: 100; 47 | margin-bottom: 15px; } 48 | 49 | pre, code { 50 | font: 0.95em Menlo, monospace; 51 | color: #777; 52 | word-wrap: normal; } 53 | 54 | p code, li code { 55 | background-color: #eee; 56 | padding: 2px 4px; 57 | border-radius: 4px; } 58 | 59 | pre > code { 60 | padding: 0; } 61 | 62 | a { 63 | color: #0088cc; 64 | text-decoration: none; } 65 | a code { 66 | color: inherit; } 67 | 68 | ul { 69 | padding-left: 15px; } 70 | 71 | li { 72 | line-height: 1.8em; } 73 | 74 | img { 75 | max-width: 100%; } 76 | 77 | blockquote { 78 | margin-left: 0; 79 | padding: 0 10px; 80 | border-left: 4px solid #ccc; } 81 | 82 | .content-wrapper { 83 | margin: 0 auto; 84 | width: 980px; } 85 | 86 | header { 87 | font-size: 0.85em; 88 | line-height: 32px; 89 | background-color: #414141; 90 | position: fixed; 91 | width: 100%; 92 | z-index: 3; } 93 | header img { 94 | padding-right: 6px; 95 | vertical-align: -4px; 96 | height: 16px; } 97 | header a { 98 | color: #fff; } 99 | header p { 100 | float: left; 101 | color: #999; } 102 | header .header-right { 103 | float: right; 104 | margin-left: 16px; } 105 | 106 | #breadcrumbs { 107 | background-color: #f2f2f2; 108 | height: 21px; 109 | padding-top: 17px; 110 | position: fixed; 111 | width: 100%; 112 | z-index: 2; 113 | margin-top: 32px; } 114 | #breadcrumbs #carat { 115 | height: 10px; 116 | margin: 0 5px; } 117 | 118 | .sidebar { 119 | background-color: #f9f9f9; 120 | border: 1px solid #e2e2e2; 121 | overflow-y: auto; 122 | overflow-x: hidden; 123 | position: fixed; 124 | top: 70px; 125 | bottom: 0; 126 | width: 230px; 127 | word-wrap: normal; } 128 | 129 | .nav-groups { 130 | list-style-type: none; 131 | background: #fff; 132 | padding-left: 0; } 133 | 134 | .nav-group-name { 135 | border-bottom: 1px solid #e2e2e2; 136 | font-size: 1.1em; 137 | font-weight: 100; 138 | padding: 15px 0 15px 20px; } 139 | .nav-group-name > a { 140 | color: #333; } 141 | 142 | .nav-group-tasks { 143 | margin-top: 5px; } 144 | 145 | .nav-group-task { 146 | font-size: 0.9em; 147 | list-style-type: none; 148 | white-space: nowrap; } 149 | .nav-group-task a { 150 | color: #888; } 151 | 152 | .main-content { 153 | background-color: #fff; 154 | border: 1px solid #e2e2e2; 155 | margin-left: 246px; 156 | position: absolute; 157 | overflow: hidden; 158 | padding-bottom: 20px; 159 | top: 70px; 160 | width: 734px; } 161 | .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { 162 | margin-bottom: 1em; } 163 | .main-content p { 164 | line-height: 1.8em; } 165 | .main-content section .section:first-child { 166 | margin-top: 0; 167 | padding-top: 0; } 168 | .main-content section .task-group-section .task-group:first-of-type { 169 | padding-top: 10px; } 170 | .main-content section .task-group-section .task-group:first-of-type .section-name { 171 | padding-top: 15px; } 172 | .main-content section .heading:before { 173 | content: ""; 174 | display: block; 175 | padding-top: 70px; 176 | margin: -70px 0 0; } 177 | .main-content .section-name p { 178 | margin-bottom: inherit; 179 | line-height: inherit; } 180 | .main-content .section-name code { 181 | background-color: inherit; 182 | padding: inherit; 183 | color: inherit; } 184 | 185 | .section { 186 | padding: 0 25px; } 187 | 188 | .highlight { 189 | background-color: #eee; 190 | padding: 10px 12px; 191 | border: 1px solid #e2e2e2; 192 | border-radius: 4px; 193 | overflow-x: auto; } 194 | 195 | .declaration .highlight { 196 | overflow-x: initial; 197 | padding: 0 40px 40px 0; 198 | margin-bottom: -25px; 199 | background-color: transparent; 200 | border: none; } 201 | 202 | .section-name { 203 | margin: 0; 204 | margin-left: 18px; } 205 | 206 | .task-group-section { 207 | margin-top: 10px; 208 | padding-left: 6px; 209 | border-top: 1px solid #e2e2e2; } 210 | 211 | .task-group { 212 | padding-top: 0px; } 213 | 214 | .task-name-container a[name]:before { 215 | content: ""; 216 | display: block; 217 | padding-top: 70px; 218 | margin: -70px 0 0; } 219 | 220 | .section-name-container { 221 | position: relative; 222 | display: inline-block; } 223 | .section-name-container .section-name-link { 224 | position: absolute; 225 | top: 0; 226 | left: 0; 227 | bottom: 0; 228 | right: 0; 229 | margin-bottom: 0; } 230 | .section-name-container .section-name { 231 | position: relative; 232 | pointer-events: none; 233 | z-index: 1; } 234 | .section-name-container .section-name a { 235 | pointer-events: auto; } 236 | 237 | .item { 238 | padding-top: 8px; 239 | width: 100%; 240 | list-style-type: none; } 241 | .item a[name]:before { 242 | content: ""; 243 | display: block; 244 | padding-top: 70px; 245 | margin: -70px 0 0; } 246 | .item code { 247 | background-color: transparent; 248 | padding: 0; } 249 | .item .token, .item .direct-link { 250 | display: inline-block; 251 | text-indent: -20px; 252 | padding-left: 3px; 253 | margin-left: 35px; 254 | font-size: 11.9px; 255 | transition: all 300ms; } 256 | .item .token-open { 257 | margin-left: 20px; } 258 | .item .discouraged { 259 | text-decoration: line-through; } 260 | .item .declaration-note { 261 | font-size: .85em; 262 | color: gray; 263 | font-style: italic; } 264 | 265 | .pointer-container { 266 | border-bottom: 1px solid #e2e2e2; 267 | left: -23px; 268 | padding-bottom: 13px; 269 | position: relative; 270 | width: 110%; } 271 | 272 | .pointer { 273 | background: #f9f9f9; 274 | border-left: 1px solid #e2e2e2; 275 | border-top: 1px solid #e2e2e2; 276 | height: 12px; 277 | left: 21px; 278 | top: -7px; 279 | -webkit-transform: rotate(45deg); 280 | -moz-transform: rotate(45deg); 281 | -o-transform: rotate(45deg); 282 | transform: rotate(45deg); 283 | position: absolute; 284 | width: 12px; } 285 | 286 | .height-container { 287 | display: none; 288 | left: -25px; 289 | padding: 0 25px; 290 | position: relative; 291 | width: 100%; 292 | overflow: hidden; } 293 | .height-container .section { 294 | background: #f9f9f9; 295 | border-bottom: 1px solid #e2e2e2; 296 | left: -25px; 297 | position: relative; 298 | width: 100%; 299 | padding-top: 10px; 300 | padding-bottom: 5px; } 301 | 302 | .aside, .language { 303 | padding: 6px 12px; 304 | margin: 12px 0; 305 | border-left: 5px solid #dddddd; 306 | overflow-y: hidden; } 307 | .aside .aside-title, .language .aside-title { 308 | font-size: 9px; 309 | letter-spacing: 2px; 310 | text-transform: uppercase; 311 | padding-bottom: 0; 312 | margin: 0; 313 | color: #aaa; 314 | -webkit-user-select: none; } 315 | .aside p:last-child, .language p:last-child { 316 | margin-bottom: 0; } 317 | 318 | .language { 319 | border-left: 5px solid #cde9f4; } 320 | .language .aside-title { 321 | color: #4b8afb; } 322 | 323 | .aside-warning, .aside-deprecated, .aside-unavailable { 324 | border-left: 5px solid #ff6666; } 325 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 326 | color: #ff0000; } 327 | 328 | .graybox { 329 | border-collapse: collapse; 330 | width: 100%; } 331 | .graybox p { 332 | margin: 0; 333 | word-break: break-word; 334 | min-width: 50px; } 335 | .graybox td { 336 | border: 1px solid #e2e2e2; 337 | padding: 5px 25px 5px 10px; 338 | vertical-align: middle; } 339 | .graybox tr td:first-of-type { 340 | text-align: right; 341 | padding: 7px; 342 | vertical-align: top; 343 | word-break: normal; 344 | width: 40px; } 345 | 346 | .slightly-smaller { 347 | font-size: 0.9em; } 348 | 349 | #footer { 350 | position: relative; 351 | top: 10px; 352 | bottom: 0px; 353 | margin-left: 25px; } 354 | #footer p { 355 | margin: 0; 356 | color: #aaa; 357 | font-size: 0.8em; } 358 | 359 | html.dash header, html.dash #breadcrumbs, html.dash .sidebar { 360 | display: none; } 361 | 362 | html.dash .main-content { 363 | width: 980px; 364 | margin-left: 0; 365 | border: none; 366 | width: 100%; 367 | top: 0; 368 | padding-bottom: 0; } 369 | 370 | html.dash .height-container { 371 | display: block; } 372 | 373 | html.dash .item .token { 374 | margin-left: 0; } 375 | 376 | html.dash .content-wrapper { 377 | width: auto; } 378 | 379 | html.dash #footer { 380 | position: static; } 381 | 382 | form[role=search] { 383 | float: right; } 384 | form[role=search] input { 385 | font: Helvetica, freesans, Arial, sans-serif; 386 | margin-top: 6px; 387 | font-size: 13px; 388 | line-height: 20px; 389 | padding: 0px 10px; 390 | border: none; 391 | border-radius: 1em; } 392 | .loading form[role=search] input { 393 | background: white url(../img/spinner.gif) center right 4px no-repeat; } 394 | form[role=search] .tt-menu { 395 | margin: 0; 396 | min-width: 300px; 397 | background: #fff; 398 | color: #333; 399 | border: 1px solid #e2e2e2; 400 | z-index: 4; } 401 | form[role=search] .tt-highlight { 402 | font-weight: bold; } 403 | form[role=search] .tt-suggestion { 404 | font: Helvetica, freesans, Arial, sans-serif; 405 | font-size: 14px; 406 | padding: 0 8px; } 407 | form[role=search] .tt-suggestion span { 408 | display: table-cell; 409 | white-space: nowrap; } 410 | form[role=search] .tt-suggestion .doc-parent-name { 411 | width: 100%; 412 | text-align: right; 413 | font-weight: normal; 414 | font-size: 0.9em; 415 | padding-left: 16px; } 416 | form[role=search] .tt-suggestion:hover, 417 | form[role=search] .tt-suggestion.tt-cursor { 418 | cursor: pointer; 419 | background-color: #4183c4; 420 | color: #fff; } 421 | form[role=search] .tt-suggestion:hover .doc-parent-name, 422 | form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { 423 | color: #fff; } 424 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NWWebSocket Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

NWWebSocket 0.5.2 Docs (100% documented)

20 |

View on GitHub

21 |

22 |

23 | 24 |
25 |

26 |
27 |
28 |
29 | 34 |
35 |
36 | 59 |
60 |
61 |
62 | 63 |

NWWebSocket

64 | 65 |

Build Status 66 | Latest Release 67 | API Docs 68 | Supported Platforms 69 | Swift Versions 70 | Cocoapods Compatible 71 | Carthage Compatible 72 | Twitter 73 | GitHub license

74 | 75 |

A WebSocket client written in Swift, using the Network framework from Apple.

76 | 77 | 86 |

Supported platforms

87 | 88 |
    89 |
  • Swift 5.1 and above
  • 90 |
  • Xcode 11.0 and above
  • 91 |
92 |

Deployment targets

93 | 94 |
    95 |
  • iOS 13.0 and above
  • 96 |
  • macOS 10.15 and above
  • 97 |
  • tvOS 13.0 and above
  • 98 |
  • watchOS 6.0 and above
  • 99 |
100 |

Installation

101 |

CocoaPods

102 | 103 |

CocoaPods is a dependency manager for Cocoa projects.

104 | 105 |

If you don’t already have the Cocoapods gem installed, run the following command:

106 |
$ gem install cocoapods
107 | 
108 | 109 |

To integrate NWWebSocket into your Xcode project using CocoaPods, specify it in your Podfile:

110 |
source 'https://github.com/CocoaPods/Specs.git'
111 | platform :ios, '14.0'
112 | use_frameworks!
113 | 
114 | pod 'NWWebSocket', '~> 0.5.2'
115 | 
116 | 117 |

Then, run the following command:

118 |
$ pod install
119 | 
120 | 121 |

If you find that you’re not having the most recent version installed when you run pod install then try running:

122 |
$ pod cache clean
123 | $ pod repo update NWWebSocket
124 | $ pod install
125 | 
126 | 127 |

Also you’ll need to make sure that you’ve not got the version of NWWebSocket locked to an old version in your Podfile.lock file.

128 |

Swift Package Manager

129 | 130 |

To integrate the library into your project using Swift Package Manager, you can add the library as a dependency in Xcode – see the docs. The package repository URL is:

131 |
https://github.com/pusher/NWWebSocket.git
132 | 
133 | 134 |

Alternatively, you can add the library as a dependency in your Package.swift file. For example:

135 |
// swift-tools-version:5.1
136 | import PackageDescription
137 | 
138 | let package = Package(
139 |     name: "YourPackage",
140 |     products: [
141 |         .library(
142 |             name: "YourPackage",
143 |             targets: ["YourPackage"]),
144 |     ],
145 |     dependencies: [
146 |         .package(url: "https://github.com/pusher/NWWebSocket.git",
147 |                  .upToNextMajor(from: "0.5.2")),
148 |     ],
149 |     targets: [
150 |         .target(
151 |             name: "YourPackage",
152 |             dependencies: ["NWWebSocket"]),
153 |     ]
154 | )
155 | 
156 | 157 |

You will then need to include both import Network and import NWWebSocket statements in any source files where you wish to use the library.

158 |

Usage

159 | 160 |

This section describes how to configure and use NWWebSocket to manage a WebSocket connection.

161 |

Connection and disconnection

162 | 163 |

Connection and disconnection is straightforward. Connecting to a WebSocket is manual by default, setting connectAutomatically to true makes connection automatic.

164 |

Manual connection

165 |
let socketURL = URL(string: "wss://somewebsockethost.com")
166 | let socket = NWWebSocket(url: socketURL)
167 | socket.delegate = self
168 | socket.connect()
169 | 
170 | // Use the WebSocket…
171 | 
172 | socket.disconnect()
173 | 
174 |

Automatic connection

175 |
let socketURL = URL(string: "wss://somewebsockethost.com")
176 | let socket = NWWebSocket(url: socketURL, connectAutomatically: true)
177 | socket.delegate = self
178 | 
179 | // Use the WebSocket…
180 | 
181 | socket.disconnect()
182 | 
183 | 184 |

NOTES:

185 | 186 | 189 |

Sending data

190 | 191 |

UTF-8 encoded strings or binary data can be sent over the WebSocket connection.

192 |
// Sending a `String`
193 | let message = "Hello, world!"
194 | socket.send(string: message)
195 | 
196 | // Sending some binary data
197 | let data: [UInt8] = [123, 234]
198 | let messageData = Data(data)
199 | socket.send(data: messageData)
200 | 
201 |

Receiving messages and connection updates

202 | 203 |

String or data messages (as well as connection state updates) can be received by making a type you define conform to WebSocketConnectionDelegate. You can then respond to received messages or connection events accordingly.

204 |
extension MyWebSocketConnectionManager: WebSocketConnectionDelegate {
205 | 
206 |     func webSocketDidConnect(connection: WebSocketConnection) {
207 |         // Respond to a WebSocket connection event
208 |     }
209 | 
210 |     func webSocketDidDisconnect(connection: WebSocketConnection,
211 |                                 closeCode: NWProtocolWebSocket.CloseCode, reason: Data?) {
212 |         // Respond to a WebSocket disconnection event
213 |     }
214 | 
215 |     func webSocketViabilityDidChange(connection: WebSocketConnection, isViable: Bool) {
216 |         // Respond to a WebSocket connection viability change event
217 |     }
218 | 
219 |     func webSocketDidAttemptBetterPathMigration(result: Result<WebSocketConnection, NWError>) {
220 |         // Respond to when a WebSocket connection migrates to a better network path
221 |         // (e.g. A device moves from a cellular connection to a Wi-Fi connection)
222 |     }
223 | 
224 |     func webSocketDidReceiveError(connection: WebSocketConnection, error: NWError) {
225 |         // Respond to a WebSocket error event
226 |     }
227 | 
228 |     func webSocketDidReceivePong(connection: WebSocketConnection) {
229 |         // Respond to a WebSocket connection receiving a Pong from the peer
230 |     }
231 | 
232 |     func webSocketDidReceiveMessage(connection: WebSocketConnection, string: String) {
233 |         // Respond to a WebSocket connection receiving a `String` message
234 |     }
235 | 
236 |     func webSocketDidReceiveMessage(connection: WebSocketConnection, data: Data) {
237 |         // Respond to a WebSocket connection receiving a binary `Data` message
238 |     }
239 | }
240 | 
241 |

Ping and pong

242 | 243 |

Triggering a Ping on an active WebSocket connection is a best practice method of telling the connected peer that the connection should be maintained. Pings can be triggered on-demand or periodically.

244 |

245 | // Trigger a Ping on demand
246 | socket.ping()
247 | 
248 | // Trigger a Ping periodically
249 | // (This is useful when messages are infrequently sent across the connection to prevent a connection closure)
250 | socket.ping(interval: 30.0)
251 | 
252 |

Documentation

253 | 254 |

Full documentation of the library can be found in the API docs.

255 |

Reporting bugs and requesting features

256 | 257 |
    258 |
  • If you have found a bug or have a feature request, please open an issue
  • 259 |
  • If you want to contribute, please submit a pull request (preferably with some tests 🙂 )
  • 260 |
261 |

Credits

262 | 263 |

NWWebSocket is owned and maintained by Pusher. It was originally created by Daniel Browne.

264 | 265 |

It uses code from the following repositories:

266 | 267 | 270 |

License

271 | 272 |

NWWebSocket is released under the MIT license. See LICENSE for details.

273 | 274 |
275 |
276 | 280 |
281 |
282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/search.json: -------------------------------------------------------------------------------- 1 | {"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB10DidConnect10connectionyAA0cbD0_p_tF":{"name":"webSocketDidConnect(connection:)","abstract":"

Tells the delegate that the WebSocket did connect successfully.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB13DidDisconnect10connection9closeCode6reasonyAA0cbD0_p_7Network010NWProtocolcB0C05CloseK0O10Foundation4DataVSgtF":{"name":"webSocketDidDisconnect(connection:closeCode:reason:)","abstract":"

Tells the delegate that the WebSocket did disconnect.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB18ViabilityDidChange10connection8isViableyAA0cbD0_p_SbtF":{"name":"webSocketViabilityDidChange(connection:isViable:)","abstract":"

Tells the delegate that the WebSocket connection viability has changed.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB29DidAttemptBetterPathMigration6resultys6ResultOyAA0cbD0_p7Network7NWErrorOG_tF":{"name":"webSocketDidAttemptBetterPathMigration(result:)","abstract":"

Tells the delegate that the WebSocket has attempted a migration based on a better network path becoming available.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB15DidReceiveError10connection5erroryAA0cbD0_p_7Network7NWErrorOtF":{"name":"webSocketDidReceiveError(connection:error:)","abstract":"

Tells the delegate that the WebSocket received an error.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB14DidReceivePong10connectionyAA0cbD0_p_tF":{"name":"webSocketDidReceivePong(connection:)","abstract":"

Tells the delegate that the WebSocket received a ‘pong’ from the server.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB17DidReceiveMessage10connection6stringyAA0cbD0_p_SStF":{"name":"webSocketDidReceiveMessage(connection:string:)","abstract":"

Tells the delegate that the WebSocket received a String message.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB17DidReceiveMessage10connection4datayAA0cbD0_p_10Foundation4DataVtF":{"name":"webSocketDidReceiveMessage(connection:data:)","abstract":"

Tells the delegate that the WebSocket received a binary Data message.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP7connectyyF":{"name":"connect()","abstract":"

Connect to the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP4send6stringySS_tF":{"name":"send(string:)","abstract":"

Send a UTF-8 formatted String over the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP4send4datay10Foundation4DataV_tF":{"name":"send(data:)","abstract":"

Send some Data over the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP6listenyyF":{"name":"listen()","abstract":"

Start listening for messages over the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP4ping8intervalySd_tF":{"name":"ping(interval:)","abstract":"

Ping the WebSocket periodically.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP4pingyyF":{"name":"ping()","abstract":"

Ping the WebSocket once.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP10disconnect9closeCodey7Network010NWProtocolcB0C05CloseG0O_tF":{"name":"disconnect(closeCode:)","abstract":"

Disconnect from the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP8delegateAA0cbD8Delegate_pSgvp":{"name":"delegate","abstract":"

The WebSocket connection delegate.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html":{"name":"WebSocketConnection","abstract":"

Defines a WebSocket connection.

"},"Protocols/WebSocketConnectionDelegate.html":{"name":"WebSocketConnectionDelegate","abstract":"

Defines a delegate for a WebSocket connection.

"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC8delegateAA03WebB18ConnectionDelegate_pSgvp":{"name":"delegate","abstract":"

The WebSocket connection delegate.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC14defaultOptions7Network013NWProtocolWebB0C0D0CvpZ":{"name":"defaultOptions","abstract":"

The default NWProtocolWebSocket.Options for a WebSocket connection.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC7request20connectAutomatically7options15connectionQueueAB10Foundation10URLRequestV_Sb7Network013NWProtocolWebB0C7OptionsCSo17OS_dispatch_queueCtcfc":{"name":"init(request:connectAutomatically:options:connectionQueue:)","abstract":"

Creates a NWWebSocket instance which connects to a socket url with some configuration options.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC3url20connectAutomatically7options15connectionQueueAB10Foundation3URLV_Sb7Network013NWProtocolWebB0C7OptionsCSo17OS_dispatch_queueCtcfc":{"name":"init(url:connectAutomatically:options:connectionQueue:)","abstract":"

Creates a NWWebSocket instance which connects a socket url with some configuration options.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC7connectyyF":{"name":"connect()","abstract":"

Connect to the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC4send6stringySS_tF":{"name":"send(string:)","abstract":"

Send a UTF-8 formatted String over the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC4send4datay10Foundation4DataV_tF":{"name":"send(data:)","abstract":"

Send some Data over the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC6listenyyF":{"name":"listen()","abstract":"

Start listening for messages over the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC4ping8intervalySd_tF":{"name":"ping(interval:)","abstract":"

Ping the WebSocket periodically.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC4pingyyF":{"name":"ping()","abstract":"

Ping the WebSocket once.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC10disconnect9closeCodey7Network013NWProtocolWebB0C05CloseE0O_tF":{"name":"disconnect(closeCode:)","abstract":"

Disconnect from the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html":{"name":"NWWebSocket","abstract":"

A WebSocket client that manages a socket connection.

"},"Classes.html":{"name":"Classes","abstract":"

The following classes are available globally.

"},"Protocols.html":{"name":"Protocols","abstract":"

The following protocols are available globally.

"}} -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/Documents/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | 4 | ], 5 | "source_directory": "/Users/danielbrowne/Documents/Work/NWWebSocket" 6 | } -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/docsets/NWWebSocket.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/NWWebSocket.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/docsets/NWWebSocket.tgz -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/NWWebSocket/1e545fcb53966272fc042aa17ae932f11239e00f/docs/img/spinner.gif -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NWWebSocket Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

NWWebSocket 0.5.2 Docs (100% documented)

20 |

View on GitHub

21 |

22 |

23 | 24 |
25 |

26 |
27 |
28 |
29 | 34 |
35 |
36 | 59 |
60 |
61 |
62 | 63 |

NWWebSocket

64 | 65 |

Build Status 66 | Latest Release 67 | API Docs 68 | Supported Platforms 69 | Swift Versions 70 | Cocoapods Compatible 71 | Carthage Compatible 72 | Twitter 73 | GitHub license

74 | 75 |

A WebSocket client written in Swift, using the Network framework from Apple.

76 | 77 | 86 |

Supported platforms

87 | 88 |
    89 |
  • Swift 5.1 and above
  • 90 |
  • Xcode 11.0 and above
  • 91 |
92 |

Deployment targets

93 | 94 |
    95 |
  • iOS 13.0 and above
  • 96 |
  • macOS 10.15 and above
  • 97 |
  • tvOS 13.0 and above
  • 98 |
  • watchOS 6.0 and above
  • 99 |
100 |

Installation

101 |

CocoaPods

102 | 103 |

CocoaPods is a dependency manager for Cocoa projects.

104 | 105 |

If you don’t already have the Cocoapods gem installed, run the following command:

106 |
$ gem install cocoapods
107 | 
108 | 109 |

To integrate NWWebSocket into your Xcode project using CocoaPods, specify it in your Podfile:

110 |
source 'https://github.com/CocoaPods/Specs.git'
111 | platform :ios, '14.0'
112 | use_frameworks!
113 | 
114 | pod 'NWWebSocket', '~> 0.5.2'
115 | 
116 | 117 |

Then, run the following command:

118 |
$ pod install
119 | 
120 | 121 |

If you find that you’re not having the most recent version installed when you run pod install then try running:

122 |
$ pod cache clean
123 | $ pod repo update NWWebSocket
124 | $ pod install
125 | 
126 | 127 |

Also you’ll need to make sure that you’ve not got the version of NWWebSocket locked to an old version in your Podfile.lock file.

128 |

Swift Package Manager

129 | 130 |

To integrate the library into your project using Swift Package Manager, you can add the library as a dependency in Xcode – see the docs. The package repository URL is:

131 |
https://github.com/pusher/NWWebSocket.git
132 | 
133 | 134 |

Alternatively, you can add the library as a dependency in your Package.swift file. For example:

135 |
// swift-tools-version:5.1
136 | import PackageDescription
137 | 
138 | let package = Package(
139 |     name: "YourPackage",
140 |     products: [
141 |         .library(
142 |             name: "YourPackage",
143 |             targets: ["YourPackage"]),
144 |     ],
145 |     dependencies: [
146 |         .package(url: "https://github.com/pusher/NWWebSocket.git",
147 |                  .upToNextMajor(from: "0.5.2")),
148 |     ],
149 |     targets: [
150 |         .target(
151 |             name: "YourPackage",
152 |             dependencies: ["NWWebSocket"]),
153 |     ]
154 | )
155 | 
156 | 157 |

You will then need to include both import Network and import NWWebSocket statements in any source files where you wish to use the library.

158 |

Usage

159 | 160 |

This section describes how to configure and use NWWebSocket to manage a WebSocket connection.

161 |

Connection and disconnection

162 | 163 |

Connection and disconnection is straightforward. Connecting to a WebSocket is manual by default, setting connectAutomatically to true makes connection automatic.

164 |

Manual connection

165 |
let socketURL = URL(string: "wss://somewebsockethost.com")
166 | let socket = NWWebSocket(url: socketURL)
167 | socket.delegate = self
168 | socket.connect()
169 | 
170 | // Use the WebSocket…
171 | 
172 | socket.disconnect()
173 | 
174 |

Automatic connection

175 |
let socketURL = URL(string: "wss://somewebsockethost.com")
176 | let socket = NWWebSocket(url: socketURL, connectAutomatically: true)
177 | socket.delegate = self
178 | 
179 | // Use the WebSocket…
180 | 
181 | socket.disconnect()
182 | 
183 | 184 |

NOTES:

185 | 186 | 189 |

Sending data

190 | 191 |

UTF-8 encoded strings or binary data can be sent over the WebSocket connection.

192 |
// Sending a `String`
193 | let message = "Hello, world!"
194 | socket.send(string: message)
195 | 
196 | // Sending some binary data
197 | let data: [UInt8] = [123, 234]
198 | let messageData = Data(data)
199 | socket.send(data: messageData)
200 | 
201 |

Receiving messages and connection updates

202 | 203 |

String or data messages (as well as connection state updates) can be received by making a type you define conform to WebSocketConnectionDelegate. You can then respond to received messages or connection events accordingly.

204 |
extension MyWebSocketConnectionManager: WebSocketConnectionDelegate {
205 | 
206 |     func webSocketDidConnect(connection: WebSocketConnection) {
207 |         // Respond to a WebSocket connection event
208 |     }
209 | 
210 |     func webSocketDidDisconnect(connection: WebSocketConnection,
211 |                                 closeCode: NWProtocolWebSocket.CloseCode, reason: Data?) {
212 |         // Respond to a WebSocket disconnection event
213 |     }
214 | 
215 |     func webSocketViabilityDidChange(connection: WebSocketConnection, isViable: Bool) {
216 |         // Respond to a WebSocket connection viability change event
217 |     }
218 | 
219 |     func webSocketDidAttemptBetterPathMigration(result: Result<WebSocketConnection, NWError>) {
220 |         // Respond to when a WebSocket connection migrates to a better network path
221 |         // (e.g. A device moves from a cellular connection to a Wi-Fi connection)
222 |     }
223 | 
224 |     func webSocketDidReceiveError(connection: WebSocketConnection, error: NWError) {
225 |         // Respond to a WebSocket error event
226 |     }
227 | 
228 |     func webSocketDidReceivePong(connection: WebSocketConnection) {
229 |         // Respond to a WebSocket connection receiving a Pong from the peer
230 |     }
231 | 
232 |     func webSocketDidReceiveMessage(connection: WebSocketConnection, string: String) {
233 |         // Respond to a WebSocket connection receiving a `String` message
234 |     }
235 | 
236 |     func webSocketDidReceiveMessage(connection: WebSocketConnection, data: Data) {
237 |         // Respond to a WebSocket connection receiving a binary `Data` message
238 |     }
239 | }
240 | 
241 |

Ping and pong

242 | 243 |

Triggering a Ping on an active WebSocket connection is a best practice method of telling the connected peer that the connection should be maintained. Pings can be triggered on-demand or periodically.

244 |

245 | // Trigger a Ping on demand
246 | socket.ping()
247 | 
248 | // Trigger a Ping periodically
249 | // (This is useful when messages are infrequently sent across the connection to prevent a connection closure)
250 | socket.ping(interval: 30.0)
251 | 
252 |

Documentation

253 | 254 |

Full documentation of the library can be found in the API docs.

255 |

Reporting bugs and requesting features

256 | 257 |
    258 |
  • If you have found a bug or have a feature request, please open an issue
  • 259 |
  • If you want to contribute, please submit a pull request (preferably with some tests 🙂 )
  • 260 |
261 |

Credits

262 | 263 |

NWWebSocket is owned and maintained by Pusher. It was originally created by Daniel Browne.

264 | 265 |

It uses code from the following repositories:

266 | 267 | 270 |

License

271 | 272 |

NWWebSocket is released under the MIT license. See LICENSE for details.

273 | 274 |
275 |
276 | 280 |
281 |
282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/search.json: -------------------------------------------------------------------------------- 1 | {"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB10DidConnect10connectionyAA0cbD0_p_tF":{"name":"webSocketDidConnect(connection:)","abstract":"

Tells the delegate that the WebSocket did connect successfully.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB13DidDisconnect10connection9closeCode6reasonyAA0cbD0_p_7Network010NWProtocolcB0C05CloseK0O10Foundation4DataVSgtF":{"name":"webSocketDidDisconnect(connection:closeCode:reason:)","abstract":"

Tells the delegate that the WebSocket did disconnect.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB18ViabilityDidChange10connection8isViableyAA0cbD0_p_SbtF":{"name":"webSocketViabilityDidChange(connection:isViable:)","abstract":"

Tells the delegate that the WebSocket connection viability has changed.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB29DidAttemptBetterPathMigration6resultys6ResultOyAA0cbD0_p7Network7NWErrorOG_tF":{"name":"webSocketDidAttemptBetterPathMigration(result:)","abstract":"

Tells the delegate that the WebSocket has attempted a migration based on a better network path becoming available.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB15DidReceiveError10connection5erroryAA0cbD0_p_7Network7NWErrorOtF":{"name":"webSocketDidReceiveError(connection:error:)","abstract":"

Tells the delegate that the WebSocket received an error.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB14DidReceivePong10connectionyAA0cbD0_p_tF":{"name":"webSocketDidReceivePong(connection:)","abstract":"

Tells the delegate that the WebSocket received a ‘pong’ from the server.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB17DidReceiveMessage10connection6stringyAA0cbD0_p_SStF":{"name":"webSocketDidReceiveMessage(connection:string:)","abstract":"

Tells the delegate that the WebSocket received a String message.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnectionDelegate.html#/s:11NWWebSocket03WebB18ConnectionDelegateP03webB17DidReceiveMessage10connection4datayAA0cbD0_p_10Foundation4DataVtF":{"name":"webSocketDidReceiveMessage(connection:data:)","abstract":"

Tells the delegate that the WebSocket received a binary Data message.

","parent_name":"WebSocketConnectionDelegate"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP7connectyyF":{"name":"connect()","abstract":"

Connect to the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP4send6stringySS_tF":{"name":"send(string:)","abstract":"

Send a UTF-8 formatted String over the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP4send4datay10Foundation4DataV_tF":{"name":"send(data:)","abstract":"

Send some Data over the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP6listenyyF":{"name":"listen()","abstract":"

Start listening for messages over the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP4ping8intervalySd_tF":{"name":"ping(interval:)","abstract":"

Ping the WebSocket periodically.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP4pingyyF":{"name":"ping()","abstract":"

Ping the WebSocket once.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP10disconnect9closeCodey7Network010NWProtocolcB0C05CloseG0O_tF":{"name":"disconnect(closeCode:)","abstract":"

Disconnect from the WebSocket.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html#/s:11NWWebSocket03WebB10ConnectionP8delegateAA0cbD8Delegate_pSgvp":{"name":"delegate","abstract":"

The WebSocket connection delegate.

","parent_name":"WebSocketConnection"},"Protocols/WebSocketConnection.html":{"name":"WebSocketConnection","abstract":"

Defines a WebSocket connection.

"},"Protocols/WebSocketConnectionDelegate.html":{"name":"WebSocketConnectionDelegate","abstract":"

Defines a delegate for a WebSocket connection.

"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC8delegateAA03WebB18ConnectionDelegate_pSgvp":{"name":"delegate","abstract":"

The WebSocket connection delegate.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC14defaultOptions7Network013NWProtocolWebB0C0D0CvpZ":{"name":"defaultOptions","abstract":"

The default NWProtocolWebSocket.Options for a WebSocket connection.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC7request20connectAutomatically7options15connectionQueueAB10Foundation10URLRequestV_Sb7Network013NWProtocolWebB0C7OptionsCSo17OS_dispatch_queueCtcfc":{"name":"init(request:connectAutomatically:options:connectionQueue:)","abstract":"

Creates a NWWebSocket instance which connects to a socket url with some configuration options.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC3url20connectAutomatically7options15connectionQueueAB10Foundation3URLV_Sb7Network013NWProtocolWebB0C7OptionsCSo17OS_dispatch_queueCtcfc":{"name":"init(url:connectAutomatically:options:connectionQueue:)","abstract":"

Creates a NWWebSocket instance which connects a socket url with some configuration options.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC7connectyyF":{"name":"connect()","abstract":"

Connect to the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC4send6stringySS_tF":{"name":"send(string:)","abstract":"

Send a UTF-8 formatted String over the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC4send4datay10Foundation4DataV_tF":{"name":"send(data:)","abstract":"

Send some Data over the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC6listenyyF":{"name":"listen()","abstract":"

Start listening for messages over the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC4ping8intervalySd_tF":{"name":"ping(interval:)","abstract":"

Ping the WebSocket periodically.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC4pingyyF":{"name":"ping()","abstract":"

Ping the WebSocket once.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html#/s:11NWWebSocketAAC10disconnect9closeCodey7Network013NWProtocolWebB0C05CloseE0O_tF":{"name":"disconnect(closeCode:)","abstract":"

Disconnect from the WebSocket.

","parent_name":"NWWebSocket"},"Classes/NWWebSocket.html":{"name":"NWWebSocket","abstract":"

A WebSocket client that manages a socket connection.

"},"Classes.html":{"name":"Classes","abstract":"

The following classes are available globally.

"},"Protocols.html":{"name":"Protocols","abstract":"

The following protocols are available globally.

"}} -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | 4 | ], 5 | "source_directory": "/Users/danielbrowne/Documents/Work/NWWebSocket" 6 | } --------------------------------------------------------------------------------